diff --git a/test/Class/AbstractTest.js b/test/Class/AbstractTest.js new file mode 100644 index 0000000..66dc06f --- /dev/null +++ b/test/Class/AbstractTest.js @@ -0,0 +1,534 @@ +/** + * Tests abstract classes + * + * Copyright (C) 2010, 2011, 2012, 2013 Mike Gerwitz + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'class_abstract' ); + this.Class = this.require( 'class' ); + }, + + + /** + * In order to ensure that the code documents itself, we should require + * that all classes containing abstract members must themselves be + * declared as abstract. Otherwise, you are at the mercy of the + * developer's documentation/comments to know whether or not the class + * is indeed abstract without looking through its definition. + */ + 'Must declare classes with abstract members as abstract': function() + { + try + { + // should fail; class not declared as abstract + this.Class( 'Foo', + { + 'abstract foo': [], + } ); + } + catch ( e ) + { + this.assertOk( + e.message.search( 'Foo' ) !== -1, + "Abstract class declaration error should contain class name" + ); + + return; + } + + this.assertFail( + "Should not be able to declare abstract members unless " + + "class is also declared as abstract" + ); + }, + + + /** + * Abstract members should be permitted if the class itself is declared + * as abstract; converse of above test. + */ + 'Can declare class as abstract': function() + { + var Sut = this.Sut; + this.assertDoesNotThrow( function() + { + Sut( + { + 'abstract foo': [], + } ); + }, Error ); + }, + + + /** + * If a class is declared as abstract, it should contain at least one + * abstract method. Otherwise, the abstract definition is pointless and + * unnecessarily confusing---the whole point of the declaration is + * to produce self-documenting code. + */ + 'Abstract classes must contain abstract methods': function() + { + try + { + // should fail; class not declared as abstract + this.Sut( 'Foo', {} ); + } + catch ( e ) + { + this.assertOk( + e.message.search( 'Foo' ) !== -1, + "Abstract class declaration error should contain class name" + ); + + return; + } + + this.assertFail( + "Abstract classes should contain at least one abstract method" + ); + }, + + + /** + * Abstract methods should remain virtual until they are defined. + * That is, if a subtype doesn't provide a concrete implementation, it + * should still be considered virtual. + */ + 'Abstract methods can be defined concretely by sub-subtypes': function() + { + var AbstractFoo = this.Sut( 'Foo', + { + 'abstract foo': [], + } ), + + SubAbstractFoo = this.Sut.extend( AbstractFoo, {} ); + + var Class = this.Class; + this.assertDoesNotThrow( function() + { + Class.extend( SubAbstractFoo, + { + // we should NOT need the override keyword for concrete + // implementations of abstract super methods + 'foo': function() {}, + } ) + }, Error ); + }, + + + /** + * Just as Class contains an extend method, so should AbstractClass. + */ + 'Abstract class extend method returns new class': function() + { + this.assertEqual( typeof this.Sut.extend, 'function', + "AbstractClass contains extend method" + ); + + this.assertOk( + this.Class.isClass( + this.Sut.extend( { 'abstract foo': [] } ) + ), + "Abstract class extend method returns class" + ); + }, + + + /** + * Just as Class contains an implement method, so should AbstractClass. + * We test implementation further on in this test case. + */ + 'Abstract class contains implement method': function() + { + this.assertEqual( typeof this.Sut.implement, 'function', + "AbstractClass contains implement method" + ); + }, + + + /** + * All classes should have a method to determine if they are abstract. + * We test specific cases below. + */ + 'All classes have an isAbstract() method': function() + { + this.assertEqual( + typeof ( this.Class( {} ).isAbstract ), + 'function' + ); + }, + + + /** + * For this test, note that (as was tested above) a class containing + * abstract members must be declared as abstract; therefore, this test + * extends to assert that classes with no abstract methods are not + * considered to be abstract. + */ + 'Concrete classes are not considered to be abstract': function() + { + this.assertOk( !( this.Class( {} ).isAbstract() ) ); + }, + + + /** + * In the same spirit as the preceding test, this extends to asserting + * that a class containing abstract methods must be considered to be + * abstract. + */ + 'Abstract classes are considered to be abstract': function() + { + this.assertOk( + this.Sut( { 'abstract method': [] } ).isAbstract() + ); + }, + + + /** + * In the spirit of the aforementioned, subtypes that do not provide + * concrete definitions for *all* abstract methods of their supertype + * must too be considered to be abstract. + */ + 'Subtypes are abstract if no concrete method is provided': function() + { + var Base = this.Sut( + { + 'abstract foo': [], + 'abstract bar': [], + } ); + + this.assertOk( + this.Sut.extend( Base, + { + // only provide concrete impl. for a single method; `bar' is + // still abstract + foo: function() {} + } ).isAbstract() + ); + }, + + + /** + * Ensure that a subtype is not considered to be abstract if it provides + * concrete definitions of each of its supertype's abstract methods. + */ + 'Subtypes are not considered abstract if concrete methods are provided': + function() + { + var Base = this.Sut( + { + 'abstract foo': [], + 'abstract bar': [], + } ); + + this.assertOk( + this.Class.extend( Base, + { + // provide concrete impls. for both + foo: function() {}, + bar: function() {}, + } ).isAbstract() === false + ); + }, + + + /** + * Since an abstract class does not provide a complete object + * description, it cannot be instantiated. + */ + 'Abstract classes cannot be instantiated': function() + { + var Sut = this.Sut; + this.assertThrows( function() + { + Sut( { 'abstract foo': [] } )(); + }, Error ); + }, + + + /** + * However, a concrete subtype of an abstract class may be instantiated. + * Otherwise abstract classes would be pretty useless. + */ + 'Concrete subtypes of abstract classes can be instantiated': function() + { + var Sut = this.Sut; + this.assertDoesNotThrow( function() + { + Sut( { 'abstract foo': [] } ) + .extend( { foo: function() {} } ) + (); + }, Error ); + }, + + + /** + * Even though an abstract class itself cannot be instantiated, its + * constructor may still be inherited (and therefore invoked) through + * concrete subtypes. + */ + 'Can call constructors of abstract supertypes': function() + { + var ctor_called = false; + this.Sut( + { + __construct: function() { ctor_called = true; }, + 'abstract foo': [], + } ).extend( { foo: function() {} } )(); + + this.assertOk( ctor_called ); + }, + + + /** + * Abstract methods declare an API strictly for the purpose of ensuring + * that subtypes are all compatible with respect to that particular + * field; parameter count, therefore, should be enforced to point out + * potential bugs to developers. Whether or not the subtype makes use of + * a particular argument is a separate and unrelated issue. + */ + 'Concrete methods must implement the proper number of parameters': + function() + { + var Sut = this.Sut; + this.assertThrows( function() + { + // concrete implementation missing parameter `two' + Sut( { 'abstract foo': [ 'one', 'two' ] } ) + .extend( { foo: function( one ) {} } ); + }, Error ); + }, + + + /** + * It may be the case that a subtype wishes to provide a new definition + * for a particular abstract method---without providing a concrete + * implementation---to add additional parameters. However, to remain + * compatible with the supertype, that implementation must provide at + * least the same number of arguments as the respective method of the + * supertype. + * + * This tests the error condition; see below for the complement. + */ + 'Abstract methods of subtypes must declare compatible parameter count': + function() + { + var Sut = this.Sut; + this.assertThrows( function() + { + Sut.extend( Sut( { 'abstract foo': [ 'one' ] } ), + { + // incorrect number of arguments + 'abstract foo': [], + } ); + }, TypeError ); + }, + + + /** + * Complements the above test to ensure that compatible abstract + * overrides are permitted. + */ + 'Abstract members may implement more parameters than supertype': + function() + { + var Sut = this.Sut; + this.assertDoesNotThrow( function() + { + Sut.extend( Sut( { 'abstract foo': [ 'one' ] } ), + { + // one greater + 'abstract foo': [ 'one', 'two' ], + } ); + }, Error ); + }, + + + /** + * While this may not necessarily be sensical in all situations, it may + * be useful for documentation. + */ + 'Abstract members may implement equal parameters to supertype': + function() + { + var Sut = this.Sut; + this.assertDoesNotThrow( function() + { + Sut.extend( Sut( { 'abstract foo': [ 'one' ] } ), + { + // same number + 'abstract foo': [ 'one' ], + } ); + }, Error ); + }, + + + /** + * This test just ensures consistency by ensuring that an empty + * parameter definition for abstract methods imposes no parameter count + * requirement on its concrete definition. + */ + 'Concrete methods have no parameter requirement with empty definition': + function() + { + var Sut = this.Sut; + this.assertDoesNotThrow( function() + { + Sut( { 'abstract foo': [] } ).extend( + { + foo: function() {} + } ); + }, Error ); + }, + + + /** + * An abstract method is represented by an array listing its parameters + * (that must be implemented by concrete definitions). + */ + 'Abstract methods must be declared as arrays': function() + { + var Class = this.Class; + + this.assertThrows( function() + { + // likely demonstrates misunderstanding of the syntax + Class.extend( { 'abstract foo': function() {} } ); + }, TypeError, "Abstract method cannot be declared as a function" ); + + this.assertThrows( function() + { + // might be common mistake for attempting to denote a single + // parameter; pure speculation. + Class.extend( { 'abstract foo': 'scalar' } ); + }, TypeError, "Abstract method cannot be declared as a scalar" ); + }, + + + /** + * There was an issue where the object holding the abstract methods list + * was not checking for methods by using hasOwnProperty(). Therefore, if + * a method such as toString() was defined, it would be matched in the + * abstract methods list. As such, the abstract methods count would be + * decreased, even though it was not an abstract method to begin with + * (nor was it removed from the list, because it was never defined in + * the first place outside of the prototype). + * + * This negative number !== 0, which causes a problem when checking to + * ensure that there are 0 abstract methods. We check explicitly for 0 + * because, if it's non-zero, then it's either abstract or something is + * wrong. Negative is especially wrong. It should never be negative! + */ + 'Does not recognize object prototype members as abstract': function() + { + var Sut = this.Sut; + this.assertDoesNotThrow( function() + { + Sut( { 'abstract method': [] } ).extend( + { + // concrete, so the result would otherwise not be abstract + method: function() {}, + + // the problem---this exists in the prototype chain of every + // object + 'toString': function() {}, + })(); + }, Error ); + }, + + + /** + * Ensure we support named abstract class extending + */ + 'Can create named abstract subtypes': function() + { + this.assertOk( + this.Sut( 'Named' ).extend( + this.Sut( { 'abstract foo': [] } ), + {} + ).isAbstract() + ); + }, + + + /** + * Abstract classes, when extended, should yield a concrete class by + * default. Otherwise, the user should once again use AbstractClass to + * clearly state that the subtype is abstract. Remember: + * self-documenting. + */ + 'Calling extend() on abstract class yields concrete class': function() + { + var Foo = this.Sut( { 'abstract foo': [] } ), + cls_named = this.Sut( 'NamedSubFoo' ).extend( Foo, {} ), + cls_anon = this.Sut.extend( Foo, {} ); + + var Class = this.Class; + + // named + this.assertThrows( + function() + { + // should throw an error, since we're not declaring it as + // abstract and we're not providing a concrete impl + Class.isAbstract( cls_named.extend( {} ) ); + }, + TypeError, + "Extending named abstract classes should be concrete" + ); + + // anonymous + this.assertThrows( + function() + { + // should throw an error, since we're not declaring it as abstract + // and we're not providing a concrete impl + Class.isAbstract( cls_anon.extend( {} ) ); + }, + TypeError, + "Extending anonymous abstract classes should be concrete" + ); + }, + + + /** + * Extending an abstract class after an implement() should still result + * in an abstract class. Essentially, we are testing to ensure that the + * extend() method is properly wrapped to flag the resulting class as + * abstract. This was a bug. + */ + 'Implementing interfaces will preserve abstract class definition': + function() + { + var Sut = this.Sut, + Interface = this.require( 'interface' ); + + this.assertOk( + // if not considered abstract, extend() will fail, as it will + // contain abstract member foo + Sut( 'TestImplExtend' ) + .implement( Interface( { foo: [] } ) ) + .extend( {} ) + .isAbstract() + ); + }, +} ); diff --git a/test/test-class-abstract.js b/test/test-class-abstract.js deleted file mode 100644 index 9837be2..0000000 --- a/test/test-class-abstract.js +++ /dev/null @@ -1,468 +0,0 @@ -/** - * Tests abstract classes - * - * Copyright (C) 2010, 2011, 2012, 2013 Mike Gerwitz - * - * This file is part of GNU ease.js. - * - * ease.js is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -var common = require( './common' ), - assert = require( 'assert' ), - util = common.require( 'util' ), - - Class = common.require( 'class' ), - AbstractClass = common.require( 'class_abstract' ), - Interface = common.require( 'interface' ) -; - - -/** - * In order to ensure the code documents itself, we should require that all - * classes containing abstract members must themselves be declared as abstract. - * Otherwise, you are at the mercy of the developer's documentation/comments to - * know whether or not the class is indeed abstract without looking through its - * definition. - */ -( function testMustDeclareClassesWithAbstractMembersAsAbstract() -{ - try - { - // should fail; class not declared as abstract - Class( 'Foo', - { - 'abstract foo': [], - } ); - } - catch ( e ) - { - assert.ok( - e.message.search( 'Foo' ) !== -1, - "Abstract class declaration error should contain class name" - ); - - return; - } - - assert.fail( - "Should not be able to declare abstract members unless class is also " + - "declared as abstract" - ); -} )(); - - -/** - * Abstract members should be permitted if the class itself is declared as - * abstract - */ -( function testCanDeclareClassAsAbstract() -{ - AbstractClass( - { - 'abstract foo': [], - } ); -} )(); - - -/** - * If a class is declared as abstract, it should contain at least one abstract - * method. Otherwise, the abstract definition is pointless and unnecessarily - * confusing. The whole point of the declaration is self-documenting code. - */ -( function testAbstractClassesMustContainAbstractMethods() -{ - try - { - // should fail; class not declared as abstract - AbstractClass( 'Foo', {} ); - } - catch ( e ) - { - assert.ok( - e.message.search( 'Foo' ) !== -1, - "Abstract class declaration error should contain class name" - ); - - return; - } - - assert.fail( - "Abstract classes should contain at least one abstract method" - ); -} )(); - - -/** - * Abstract methods should remain virtual until they are overridden. That is, if - * a subtype doesn't provide a concrete implementation, it should still be - * considered virtual. - */ -( function testAbstractMethodsCanBeOverriddenBySubSubTypes() -{ - var AbstractFoo = AbstractClass( 'Foo', - { - 'abstract foo': [], - } ), - - SubAbstractFoo = AbstractClass.extend( AbstractFoo, {} ), - - ConcreteFoo = Class.extend( SubAbstractFoo, - { - // we should NOT need the override keyword for concrete - // implementations of abstract super methods - 'foo': function() {}, - } ) - ; -} )(); - - -/** - * Just as Class contains an extend method, so should AbstractClass. - */ -( function testAbstractClassExtendMethodReturnsNewClass() -{ - assert.ok( typeof AbstractClass.extend === 'function', - "AbstractClass contains extend method" - ); - - assert.ok( - Class.isClass( - AbstractClass.extend( { 'abstract foo': [] } ) - ), - "Abstract class extend method returns class" - ); -} )(); - - -/** - * Just as Class contains an implement method, so should AbstractClass. - */ -( function testAbstractClassContainsImplementMethod() -{ - assert.ok( typeof AbstractClass.implement === 'function', - "AbstractClass contains implement method" - ); -} )(); - - -// not abstract -var Foo = Class.extend( {} ); - -// abstract (ctor_called is not a class member to ensure that visibility bugs do -// not impact our test) -var ctor_called = false, - AbstractFoo = AbstractClass.extend( - { - __construct: function() - { - ctor_called = true; - }, - - 'abstract method': [ 'one', 'two', 'three' ], - - 'abstract second': [], - }) -; - -// still abstract (didn't provide a concrete implementation of both abstract -// methods) -var SubAbstractFoo = AbstractClass.extend( AbstractFoo, -{ - second: function() - { - }, -}); - -// concrete -var ConcreteFoo = Class.extend( AbstractFoo, -{ - method: function( one, two, three ) - { - // prevent Closure Compiler from optimizing the arguments away, causing - // a definition failure - return [ one, two, three ]; - }, - - second: function() - { - }, -}); - - -/** - * All classes should have a method to determine if they are abstract. - */ -( function testAllClassesHaveAMethodToDetmineIfAbstract() -{ - assert.ok( - ( Class( {} ).isAbstract instanceof Function ), - "All classes should have an isAbstract() method" - ); -} )(); - - -( function testClassesAreNotConsideredToBeAbstractIfTheyHaveNoAbstractMethods() -{ - assert.equal( - Class( {} ).isAbstract(), - false, - "Classes are not abstract if they contain no abstract methods" - ); -} )(); - - -( function testClassesShouldBeConsideredAbstractIfTheyContainAbstractMethods() -{ - assert.equal( - AbstractFoo.isAbstract(), - true, - "Classes should be considered abstract if they contain any abstract methods" - ); -} )(); - - -( function testSubtypesAreAbstractIfNoConcreteMethodIsProvided() -{ - assert.equal( - SubAbstractFoo.isAbstract(), - true, - "Subtypes of abstract types are abstract if they don't provide a " + - "concrete implementation for all abstract methods" - ); -} )(); - - -( function testSubtypesAreNotConisderedAbstractIfConcreteImplIsProvided() -{ - assert.equal( - ConcreteFoo.isAbstract(), - false, - "Subtypes of abstract types are not abstract if they provide concrete " + - "implementations of all abstract methods" - ); -} )(); - - -( function testAbstractClassesCannotBeInstantiated() -{ - assert['throws']( function() - { - // both should fail - AbstractFoo(); - SubAbstractFoo(); - }, Error, "Abstract classes cannot be instantiated" ); -} )(); - - -( function testConcreteSubclassesCanBeInstantiated() -{ - assert.ok( - ConcreteFoo(), - "Concrete subclasses can be instantiated" - ); -} )(); - - -( function testCanCallConstructorsOfAbstractSupertypes() -{ - ctor_called = false; - ConcreteFoo(); - - assert.equal( - ctor_called, - true, - "Can call constructors of abstract supertypes" - ); -} )(); - - -( function testConcreteMethodsMustImplementTheProperNumberOfArguments() -{ - assert['throws']( function() - { - AbstractFoo.extend( - { - // incorrect number of arguments - method: function() - { - }, - }); - }, Error, "Concrete methods must implement the proper number of argments" ); -} )(); - - -( function testAbstractMethodsOfSubtypesMustImplementProperNumberOfArguments() -{ - assert['throws']( - function() - { - AbstractFoo.extend( - { - // incorrect number of arguments - 'abstract method': [], - }); - }, - TypeError, - "Abstract methods of subtypes must implement the proper number of " + - "argments" - ); -} )(); - - -( function testAbstractMembersMayImplementMoreArgumentsThanSupertype() -{ - assert.doesNotThrow( - function() - { - AbstractClass.extend( AbstractFoo, - { - // incorrect number of arguments - 'abstract method': [ 'one', 'two', 'three', 'four' ], - }); - }, - Error, - "Abstract methods of subtypes may implement additional arguments, " + - "so long as they implement at least the required number of " + - "arguments as defined by it supertype" - ); -} )(); - - -( function testConcreteMethodsHaveNoArgumentRequirementsIfNoDefinitionGiven() -{ - assert.doesNotThrow( - function() - { - AbstractClass.extend( AbstractFoo, - { - second: function( foo ) - { - }, - }); - }, - Error, - "Concrete methods needn't implement the proper number of arguments " + - "if no definition was provided" - ); -} )(); - - -( function testAbstractMethodsMustBeDeclaredAsArrays() -{ - assert['throws']( function() - { - Class.extend( - { - // not an array (invalid) - 'abstract foo': 'scalar', - } ); - }, TypeError, "Abstract methods must be declared as arrays" ); -} )(); - - -/** - * There was an issue where the object holding the abstract methods list was not - * checking for methods by using hasOwnProperty(). Therefore, if a method such - * as toString() was defined, it would be matched in the abstract methods list. - * As such, the abstract methods count would be decreased, even though it was - * not an abstract method to begin with (nor was it removed from the list, - * because it was never defined in the first place outside of the prototype). - * - * This negative number !== 0, which causes a problem when checking to ensure - * that there are 0 abstract methods. We check explicitly for 0 for two reasons: - * (a) it's faster than <, and (b - most importantly) if it's non-zero, then - * it's either abstract or something is wrong. Negative is especially wrong. It - * should never be negative! - */ -( function testDoesNotRecognizeObjectPrototypeMembersAsAbstractWhenDefining() -{ - assert.doesNotThrow( function() - { - Class.extend( SubAbstractFoo, - { - // concrete, so the result would otherwise not be abstract (return - // args so they're not optimized away during compile) - 'method': function( _, __, ___ ) { return [ _, __, ___ ]; }, - - // the problem - 'toString': function() {}, - })(); - }, Error, "Should not throw error if overriding a prototype method" ); -} )(); - - -/** - * Ensure we support named abstract class extending - */ -( function testCanCreateNamedAbstractSubtypes() -{ - assert.doesNotThrow( function() - { - var cls = AbstractClass( 'NamedSubFoo' ).extend( AbstractFoo, {} ); - }, Error, "Can create named abstract subtypes" ); -} )(); - - -/** - * Abstract classes, when extended, should yield a concrete class by default. - * Otherwise, the user should once again use AbstractClass to clearly state that - * the subtype is abstract. - */ -( function testExtendingAbstractClassIsNotAbstractByDefault() -{ - var cls_named = AbstractClass( 'NamedSubFoo' ).extend( AbstractFoo, {} ), - anon_named = AbstractClass.extend( AbstractFoo, {} ); - - // named - assert['throws']( - function() - { - // should throw an error, since we're not declaring it as abstract - // and we're not providing a concrete impl - Class.isAbstract( cls_named.extend( {} ) ); - }, - TypeError, - "Extending named abstract classes should be concrete by default" - ); - - // anonymous - assert['throws']( - function() - { - // should throw an error, since we're not declaring it as abstract - // and we're not providing a concrete impl - Class.isAbstract( AbstractFoo.extend( {} ) ); - }, - TypeError, - "Extending anonymous abstract classes should be concrete by default" - ); -} )(); - - -/** - * Extending an abstract class after an implement() should still result in an - * abstract class. Essentially, we are testing to ensure that the extend() - * method is properly wrapped to flag the resulting class as abstract. This was - * a bug. - */ -( function testImplementingInterfacesWillPreserveAbstractClassDeclaration() -{ - // if not considered abstract, extend() will fail, as it will contain - // abstract member foo - AbstractClass( 'TestImplExtend' ) - .implement( Interface( { foo: [] } ) ) - .extend( {} ); -} )() -