The Principle of Subcontracting


The client that sends a message to an object using a variable of CourseSession type expects it to be interpreted and acted upon in a certain manner. In test-driven development, unit tests define this "manner." A unit test describes the behavior supported by interacting with a class through its interface. It describes the behavior by first actually effecting that behavior, then by ensuring that any number of postconditions hold true upon completion.

For a class to properly derive from CourseSession, it must not change the expectations of behavior. Any postcondition that held true for a test of CourseSession should also hold true for a test executing against a subclass of CourseSession. In general, subclasses should keep or strengthen postconditions. Subclasses should extend the behavior of their superclasses, meaning that postconditions should be added to tests for the subclass.

What this means for TDD is that you should specify contracts (in the form of unit tests) at the level of the superclass. All subclasses will need to conform to these unit tests. You will build these contracts using a pattern known as Abstract Test.[5]

[5] [George2002].

Technically, there is no compelling reason to make this refactoring. The tests for CourseSession class could retain the contract for both CourseSession and SummerCourseSession. Minor differences do exist, though. The CourseSession class explicitly tracks the number of instances created; this is something the SummerCourseSession class need not do.

Building the Session superclass results in a slightly cleaner and easier to understand hierarchy. Conceptually, a CourseSession seems to be an object at the same hierarchical "level" as a SummerCourseSession.

It's up to you to decide if such a refactoring is worthwhile. If it gives you a simpler solution, go for it. If it creates artificial classes in the hierarchy without any benefit, it's a waste of time.

For the CourseSession example, you will refactor such that both CourseSession and SummerCourseSession inherit from a common abstract superclass, instead of SummerCourseSession inheriting from CourseSession. See Figure 6.4.

Figure 6.4. The Session Hierarchy


Create an abstract test class, SessionTest, that specifies the base unit tests for Session and its subclasses. Move testCreate, testComparable, and test-EnrollStudents from CourseSessionTest into SessionTest. These tests represent the contracts that should apply to all Session subclasses.

SessionTest contains an abstract factory method, createSession. Subclasses of SessionTest will provide definitions for createSession that return an object of the appropriate type (CourseSession or SummerCourseSession).

 package sis.studentinfo; import junit.framework.TestCase; import java.util.*; import static sis.studentinfo.DateUtil.createDate; abstract public class SessionTest extends TestCase {    private Session session;    private Date startDate;    public static final int CREDITS = 3;    public void setUp() {       startDate = createDate(2003, 1, 6);       session = createSession("ENGL", "101", startDate);       session.setNumberOfCredits(CREDITS);    }    abstract protected Session createSession(       String department, String number, Date startDate);    public void testCreate() {       assertEquals("ENGL", session.getDepartment());       assertEquals("101", session.getNumber());       assertEquals(0, session.getNumberOfStudents());       assertEquals(startDate, session.getStartDate());    }    public void testEnrollStudents() {       Student student1 = new Student("Cain DiVoe");       session.enroll(student1);       assertEquals(CREDITS, student1.getCredits());       assertEquals(1, session.getNumberOfStudents());       assertEquals(student1, session.get(0));       Student student2 = new Student("Coralee DeVaughn");       session.enroll(student2);       assertEquals(CREDITS, student2.getCredits());       assertEquals(2, session.getNumberOfStudents());       assertEquals(student1, session.get(0));       assertEquals(student2, session.get(1));    }    public void testComparable()  {       final Date date = new Date();       Session sessionA = createSession("CMSC", "101", date);       Session sessionB = createSession("ENGL", "101", date);       assertTrue(sessionA.compareTo(sessionB) < 0);       assertTrue(sessionB.compareTo(sessionA) > 0);       Session sessionC = createSession("CMSC", "101", date);       assertEquals(0, sessionA.compareTo(sessionC));       Session sessionD = createSession("CMSC", "210", date);       assertTrue(sessionC.compareTo(sessionD) < 0);       assertTrue(sessionD.compareTo(sessionC) > 0);    } } 

You must change the test subclasses, CourseSessionTest and SummerCourseSessionTest, so they extend from SessionTest. As you work through the example, you may refer to Figure 6.5 as the UML basis for the test reorganization.

Figure 6.5. Abstract Test


Each test subclass will also need to provide the implementation for createSession.

 // SummerCourseSessionTest.java package sis.summer; import junit.framework.*; import java.util.*; import sis.studentinfo.*; public class SummerCourseSessionTest extends SessionTest {    public void testEndDate() {       Date startDate = DateUtil.createDate(2003, 6, 9);       Session session = createSession("ENGL", "200", startDate);       Date eightWeeksOut = DateUtil.createDate(2003, 8, 1);       assertEquals(eightWeeksOut, session.getEndDate());    }    protected Session createSession(          String department,          String number,          Date date) {       return SummerCourseSession.create(department, number, date);    } } 

In CourseSessionTest, you'll need to change any usages of the createCourseSession method to createSession.

 // CourseSessionTest.java package sis.studentinfo; import junit.framework.TestCase; import java.util.*; import static sis.studentinfo.DateUtil.createDate; public class CourseSessionTest extends SessionTest {    public void testCourseDates() {       Date startDate = DateUtil.createDate(2003, 1, 6);       Session session = createSession("ENGL", "200", startDate);       Date sixteenWeeksOut = createDate(2003, 4, 25);       assertEquals(sixteenWeeksOut, session.getEndDate());    }    public void testCount() {       CourseSession.resetCount();       createSession("", "", new Date());       assertEquals(1, CourseSession.getCount());       createSession("", "", new Date());       assertEquals(2, CourseSession.getCount());    }    protected Session createSession(          String department,          String number,          Date date) {       return CourseSession.create(department, number, date);    } } 

The method testCourseDates creates its own local instance of a CourseSession. The alternative would be to use the CourseSession object created by the setUp method in SessionTest. Creating the CourseSession directly in the subclass test method makes it easier to read the test, however. It would be difficult to understand the meaning of the test were the instantiation in another class. The assertion is closely related to the setup of the CourseSession.

Both CourseSessionTest and SummerCourseSessionTest now provide an implementation for the abstract createSession method.

The most important thing to note is that CourseSessionTest and SummerCourseSessionTest share all tests defined in SessionTest, since both classes extend from SessionTest. JUnit executes each test defined in SessionTest twiceonce for a CourseSessionTest instance and once for a SummerCourseSessionTest instance. This ensures that both subclasses of Session behave appropriately.

When JUnit executes a method defined in SessionTest, the code results in a call to createSession. Since you defined createSession as abstract, there is no definition for the method in SessionTest. The Java VM calls the createSession method in the appropriate test subclass instead. Code in createSession creates an appropriate subtype of Session, either CourseSession or SummerCourseSession.

The refactoring of the production classes (I've removed the Java API documentation):

 package sis.studentinfo; import java.util.*; abstract public class Session implements Comparable<Session> {    private static int count;    private String department;    private String number;    private List<Student> students = new ArrayList<Student>();    private Date startDate;    private int numberOfCredits;    protected Session(          String department, String number, Date startDate) {       this.department = department;       this.number = number;       this.startDate = startDate;    }    public int compareTo(Session that) {       int compare =          this.getDepartment().compareTo(that.getDepartment());       if (compare != 0)          return compare;       return this.getNumber().compareTo(that.getNumber());    }    void setNumberOfCredits(int numberOfCredits) {       this.numberOfCredits = numberOfCredits;    }    public String getDepartment() {       return department;    }    public String getNumber() {       return number;    }    int getNumberOfStudents() {       return students.size();    }    public void enroll(Student student) {       student.addCredits(numberOfCredits);       students.add(student);    }    Student get(int index) {       return students.get(index);    }    protected Date getStartDate() {       return startDate;    }    public List<Student> getAllStudents() {       return students;    }    abstract protected int getSessionLength();    public Date getEndDate() {       GregorianCalendar calendar = new GregorianCalendar();       calendar.setTime(getStartDate());       final int daysInWeek = 7;       final int daysFromFridayToMonday = 3;       int numberOfDays =          getSessionLength() * daysInWeek - daysFromFridayToMonday;       calendar.add(Calendar.DAY_OF_YEAR, numberOfDays);       return calendar.getTime();    } } 

The bulk of code in Session comes directly from CourseSession. The method getSessionLength becomes an abstract method, forcing subclasses to each implement it to provide the session length in weeks. Here are CourseSession and SummerCourseSession, each having been changed to inherit from Session:

 // CourseSession.java package sis.studentinfo; import java.util.*; public class CourseSession extends Session {    private static int count;    public static CourseSession create(          String department,          String number,          Date startDate) {       return new CourseSession(department, number, startDate);    }    protected CourseSession(          String department, String number, Date startDate) {       super(department, number, startDate);       CourseSession.incrementCount();    }    static private void incrementCount() {       ++count;    }    static void resetCount() {       count = 0;    }    static int getCount() {       return count;    }    protected int getSessionLength() {       return 16;    } } // SummerCourseSession.java package sis.summer; import java.util.*; import sis.studentinfo.*; public class SummerCourseSession extends Session {    public static SummerCourseSession create(          String department,          String number,          Date startDate) {       return new SummerCourseSession(department, number, startDate);    }    private SummerCourseSession(          String department,          String number,          Date startDate) {       super(department, number, startDate);    }    protected int getSessionLength() {       return 8;    } } 

All that is left in the subclasses are constructors, static methods, and template method overrides. Constructors are not inherited. Common methods, such as enroll and getAllStudents, now appear only in the Session superclass. All of the fields are common to both subclasses and thus now appear in Session. Also important is that the return type for the create method in SummerCourseSession and CourseSession is the abstract supertype Session.

The test method testCourseDates appears in both SessionTest subclasses. You could define a common assertion for this test in SessionTest, but what would the assertion be? One simple assertion would be that the end date must be later than the start date. Subclasses would strengthen that assertion by verifying against a specific date.

Another solution would be to assert that the end date is exactly n weeks, minus the appropriate number of weekend days, after the start date. The resultant code would have you duplicate the logic from getEndDate directly in the test itself.

Of perhaps more value is adding assertions against the method getSessionLength in the abstract test:

 public void testSessionLength() {    Session session = createSession(new Date());    assertTrue(session.getSessionLength() > 0); } 

By expressing postconditions clearly in the abstract class, you are establishing a subcontract that all subclasses must adhere to.

This refactoring was a considerable amount of work. I hope you took the time to incrementally apply the changes.



Agile Java. Crafting Code with Test-Driven Development
Agile Javaв„ў: Crafting Code with Test-Driven Development
ISBN: 0131482394
EAN: 2147483647
Year: 2003
Pages: 391
Authors: Jeff Langr

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net