Lesson 31
Testing Stateful Code

In this lesson, we will look at techniques for unit testing stateful code. We will use many of the same techniques we learned for testing stateless code, but there will be additional work in setting up our tests, and we must be mindful of the order in which we test things.

We will also unit test the service layer for the Class Roster application with a new testing technique called stubbing. We will use stubbed versions of the DAOs in the unit tests for the service layer. This lesson will also explain the motivation and techniques for testing a service layer using stubbed DAOs.

UNIT TESTING STATEFUL CODE

Testing stateful code is a bit more complicated than testing stateless code because, depending on the state of the code, the same set of inputs can produce a different output. For example, say you are testing the account portion of a banking application. Some of the account components include functionality to deposit money, withdraw money, and check the account balance. Now suppose you are testing this component and you want to test the deposit method. You set up your test to add $100 to the account. After depositing the $100, you want to check the balance to make sure the $100 was deposited correctly. So, you call the method to get the account balance. What should the balance be? Should it be $100?

Herein lies the complexity of testing stateful code. You have no way of knowing what the balance of the account should be if you don't know what the balance was before you started your test. What if you deposit $100 more into the account? What should the balance be now? Clearly, it should be $100 more than it was before, but if we don't know the state of the system before the test, we have no way of knowing what that value should actually be. Because we don't know the state or what the values should be, this code is stateless, which means we need a different way of testing.

The following points illustrate the main differences between testing stateless code and stateful code:

  • You must put the system in a known good state before testing stateful code. If you do not do this, you will have no way to check to see whether the actual result of the test matches your expected result. This is not necessary for stateless code.
  • You must be mindful of the order in which you test things in stateful code. Calling the same method with the same parameters three times in a row may produce three different results if the code is stateful. Stateless code produces the same result no matter how many times in a row it is called.
  • The expected results from a particular method call might depend on if and/or how many times a different method was called in stateful code. For example, the expected result from the method that gets the bank account balance depends on how many times the deposit and withdraw methods were called and the parameters used in those calls. In other words, calling one method can change the result of calling another method. This does not happen with stateless code.

You must take all of this into account when designing and implementing tests for stateful code, which makes it more difficult than testing stateless code. In the rest of this lesson, we will see how JUnit can help us with some of these difficulties, and we will implement the unit test suite for the Class Roster DAO and service layer.

SEPARATING PRODUCTION AND TEST DATA

In our Class Roster application, the DAO's only job is to store and retrieve student information without altering that data in any way. Our tests will simply make sure that the DAO stores and retrieves students as advertised.

However, our DAO implementation is tightly intertwined with the file system, and using the DAO will actively modify the data currently stored in the working application. As such, it is important that we create a way to separate the production or working data from the test data. This will require an update to your DAOImpl class to first change the ROSTER_FILE variable to a declaration, and the addition of new constructor methods to allow this separation.

First, let's change the ROSTER_FILE variable so that it exists as a declaration, not a declaration and assignment. Instead, we will use our DAOImpl constructors to assign the filename to our constant variable. This also means we need to remove ROSTER_FILE's static keyword since different instances of ClassRosterDaoFileImpl might be using different files, and thus the ROSTER_FILE becomes better suited as a nonstatic variable associated with that instance.

Update the ROSTER_FILE variable declaration in ClassRosterDaoFileImpl to the following statement:

private final String roster_file;

Note that because ROSTER_FILE is no longer a constant, we've changed its name to lowercase. Next add the constructors in Listing 31.1 to ClassRosterDaoFileImpl.

LISTING 31.1

The New ClassRosterDaoFileImpl Constructors

public ClassRosterDaoFileImpl(){
    roster_file = "roster.txt";
}
 
public ClassRosterDaoFileImpl(String rosterTextFile){
    roster_file = rosterTextFile;
}

This has created a pair of constructors. The first, no-args constructor is providing the earlier default behavior that ClassRosterDaoFileImpl was originally built upon, which is instantiation and the assignment of roster.txt to the roster_file variable. However, the second, overloaded constructor has allowed us to create ClassRosterDaoFileImpl instances that use another file, thus allowing the file reference to be injected upon construction. This is something that will be perfect for test setup and ensuring that we don't overwrite our production application data.

Adding hashCode and equals to Student

To make testing easier, we implement the equals and hashCode methods for the Student object. This will allow us to easily compare two Student objects to see if the values of their fields match.

Both equals and hashCode are methods inherited from the Object class. The default implementation of equals simply compares the heap location of two Student references to see if they are pointing to the same place on the heap, equating whether or not they are literally the same object. While useful, during testing it would be more useful to have the method work similarly to the String's overloaded version, where when comparing two Students’ contents, if the same, those two Student objects would be considered equal.

We will only use the equals method for our tests. This requires that if two objects are equal, then the hashCode values for each object must also match. As such, we must also implement the hashCode method. We won't get into all the details here, but essentially, the hashCode of an object is a unit integer value that represents the state of that object. In short, two objects that are equal to one another must have the same hashCode value.

Luckily, NetBeans (and every other IDE) will generate these methods for us and will help ensure that they are implemented correctly. In fact, NetBeans won't let you generate one without the other.

Simply follow these steps to add equals and hashCode:

  1. Open the Student class and right-click the class name to get a pop-up menu. Select the Insert Code option in the menu.
  2. Select equals() and hashCode() from the list of options.
  3. Select all four checkboxes in both sections. All the Student properties are important for this.
  4. Click the Generate button.

You should end up with two methods that look similar to the ones in Listing 31.2. Make sure all the properties are accounted for in yours, but don't worry if the numbers are a little different. The numbers are autogenerated, and it is not as important that they match exactly with the ones in our example.

LISTING 31.2

Student's New equals and hashCode Methods

@Override
public int hashCode() {
    int hash = 7;
    hash = 89 * hash + Objects.hashCode(this.firstName);
    hash = 89 * hash + Objects.hashCode(this.lastName);
    hash = 89 * hash + Objects.hashCode(this.studentId);
    hash = 89 * hash + Objects.hashCode(this.cohort);
    return hash;
}
 
@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null) {
        return false;
    }
    if (getClass() != obj.getClass()) {
        return false;
    }
    final Student other = (Student) obj;
    if (!Objects.equals(this.firstName, other.firstName)) {
        return false;
    }
    if (!Objects.equals(this.lastName, other.lastName)) {
        return false;
    }
    if (!Objects.equals(this.studentId, other.studentId)) {
        return false;
    }
    if (!Objects.equals(this.cohort, other.cohort)) {
        return false;
    }
    return true;
}

Now that you have these methods, you can assert on whole Student objects to check their equality with another Student object.

Adding toString to Student

It is also recommended to add an overridden toString method to your Student class to help with test failure messages. This toString method is added mostly for convenience. Often in error messages, JUnit will print out information about the object that failed a test. It helps if that information is readable to us, but the default toString method only really serializes the object's class name and hashcode. Both are interesting pieces of information, but not often particularly usable. Overriding this method can allow us to print out all the object's property values instead, which can allow for much better insight into issues when reading test logs.

To add your new toString method, follow the same process we used earlier for adding the equals and hashcode methods. Right-click the Student class name and select Insert Code. From the menu, select toString to display the code generation dialog. From the dialog, select all the fields and then click the Generate button to add the code that will be similar to Listing 31.3.

LISTING 31.3

Example of Student's New toString Method

@Override
public String toString() {
    return "Student{" + "firstName=" + firstName + ", lastName=" + lastName + ", studentId=" + studentId + ", cohort=" + cohort + '}';
}

Creating the Test Class

Our next step is creating the JUnit test class that will hold our test suite for our DAO. To do this, we follow similar steps to the ones done in the stateless testing exercises.

  1. In NetBeans’s Project view, right-click the ClassRosterDaoFileImpl class.
  2. Select Tools, and then select Create/Update Tests.
  3. Walk through the wizard to create a new JUnit class.
    • The name should look similar to classroster.dao.ClassRosterDaoFileImplTest.
    • Ensure that Test Packages is selected in the Location drop-down.
    • Generated Code Checkbox List should have everything selected.
    • Method Access Levels Checkbox List should have nothing selected.
    • Generated Comments Checkbox List should have nothing selected.
  4. Click OK.

Once you complete the wizard, you should be left with a new Java class similar to Listing 31.4. This class is the shell of our new test suite, and we will be creating new tests to replace the testSomeMethod method with real tests.

LISTING 31.4

ClassRosterDaoFileImplTest

public class ClassRosterDaoFileImplTest {
 
    public ClassRosterDaoFileImplTest() {
    }
 
    @BeforeAll
    public static void setUpClass() {
    }
 
    @AfterAll
    public static void tearDownClass() {
    }
 
    @BeforeEach
    public void setUp() {
    }
 
    @AfterEach
    public void tearDown() {
    }
 
    @Test
    public void testSomeMethod() {
        fail("The test case is a prototype.");
    }
 
}

The Set Up and Tear Down Methods

The JUnit Framework gives us four methods, or rather four annotations, to help get the code we are testing into a known good state.

  • setUpClass annotated with @BeforeAll
    • This static method is run once at the time the test class is initialized and can be used to set up external resources.
  • tearDownClass annotated with @AfterAll
    • This static method is run once after all the tests have been run and can be used to clean up external resources.
  • setUp annotated with @BeforeEach
    • This nonstatic method is run once before each test method in the JUnit test class. It can be used to set things to a known good state before each test.
  • tearDown annotated with @AfterEach
    • This nonstatic method is run once after each test method in the JUnit test completes. It can be used to clean up after each test.

We will use the setUp method to put our system into a known good state before each test. Setting the system to a known good state in setUp gives us everything we need right now.

To do this, we're going to add in a new ClassRosterDao property declaration to our test class and then use the setUp in the JUnit class to reference the new overloaded constructor we built in the ClassRosterDaoFileImpl with a newly created file to ensure that we aren't messing up production data and that our testDAO is effectively empty.

Update your to ClassRosterDaoFileImplTest method, as shown in Listing 31.5, to declare our new testDao object, remove the unneeded setup/teardown methods and template method, and fill setUp with our initialization code.

LISTING 31.5

Updated ClassRosterDaoFileImplTest Method

public class ClassRosterDaoFileImplTest {
 
    ClassRosterDao testDao;
 
    public ClassRosterDaoFileImplTest() {
    }
 
    @BeforeEach
    public void setUp() throws Exception{
        String testFile = "testroster.txt";
        // Create a new FileWriter for the test roster file 
        new FileWriter(testFile);
        testDao = new ClassRosterDaoFileImpl(testFile);
    }
}

This way, before every test run, we will have created a new blank testroster.txt file using the FileWriter and then used that as our fileName when instantiating our testDao. Both ensure that we are starting with a fresh, empty DAO object and minimize our interference with the normal application's data stored in the roster.txt file.

Arrange/Act/Assert for Stateful Code

We must add two general steps to the Arrange part of the unit testing process for stateful code.

  1. Put the system in a known good state using the JUnit setUp method.
  2. Create test data and add it to the system before each test is run. This can be done either in the setUp method (if the test data should be the same for all tests) or in each individual test method.

The Act and Assert parts of the testing process are identical to those for testing stateless code:

  • Act: Call the method under test with the correct test inputs
  • Assert: Verify that the expected result matched the actual result

CLASS ROSTER DAO TEST DESIGN

Our ClassRosterDao has four methods that we must test.

  • Add Student
  • Get Student
  • Get All Students
  • Remove Student

When designing tests for these methods in our DAO, we need to make sure we design and implement tests for each method. In most cases, we will use more than one per test to “arrange” our DAO's state correctly before we assert on it.

First, we must consider what the effects of each method will be on the DAO and then how we can use the other tools (DAO methods) available to us to determine whether the state was correctly changed. Let's start this process with the addStudent method.

Add Student

Let's look at the DAO interface method and comment, which is shown in Listing 31.6, before we get started on designing a test for this method.

LISTING 31.6

The DAO Interface Method and Comment

/**
 * Adds the given Student to the roster and associates it with the given
 * student id. If there is already a student associated with the given
 * student id it will return that student object, otherwise it will
 * return null.
 *
 * @param studentId id with which student is to be associated
 * @param student student to be added to the roster
 * @return the Student object previously associated with the given  
 * student id if it exists, null otherwise
 */
Student addStudent(String studentId, Student student)
     throws ClassRosterPersistenceException;

Reading through both the method and its JavaDoc comment, let's list some of the information we can gather about this method and its potential testing requirements. Here is what we know about addStudent:

  • To call this method, we will need a studentId String and a Student object.
  • After calling this method with those parameters, the student should now be stored within ClassRosterDaoFileImplTest and be associated with the given studentId.
  • If there was no student in the DAO already with that studentId, the return should be null.
  • However, if there was a student stored under that ID, then the previous Student associated will be returned.
  • There is also a chance that this method could throw a ClassRosterPersistenceException, which implies it is touching a data store (we know it should be reading and writing to the file).

All this data is something we should already know, but it is always a good idea to double- and even triple-check your assumptions when beginning the testing process.

Looking at our gathered information, the first two bullet points are particularly important for our testing purposes. In this case, they focus on the fact that addStudent changes the state by adding a new or replacing an existing student within the DAO.

However, just the state change isn't the only thing we need to focus on. We need to also understand how to assert and prove that the state change happened correctly. In this case, we use other methods to measure the effect that this method call had on the state—the change of students stored within the DAO.

Remember, the ClassRosterDaoFileImplTest has three other methods: getStudent, getAllStudents, and removeStudent. Technically, each one of these could be used to verify the effect that calling the addStudent method has on the DAO, but getStudent is particularly well suited in that it allows us to simply retrieve the student from the DAO and examine it.

Get Student

Before we use it in a test, let's refresh ourselves with what getStudent does. Listing 31.7 presents the current code for getStudent.

We know the following things about getStudent:

  • To call this method, we will need a studentId String.
  • The return should be a Student object. In theory, this is a student previously stored under that ID.
  • If there was no student in the DAO with that studentId, the return should be null.
  • There is also a chance that this method could throw a ClassRosterPersistenceException, which again implies touching a persistent data store.

ClassRosterDaoTest: Adding and Getting a Student

Our Add/Get Student test is straightforward. We know that the DAO is in an empty state since we created a new empty instance within our setUp method.

  1. The first step of this test is to create a new Student object (Arrange).
  2. Then we add that student to the DAO (Act).
  3. Next, we get the student back out of the DAO and put it in another variable (Act).
  4. Finally, we check to see that the data within the stored student is equal to the retrieved student from the DAO (Assert).

Listing 31.8 presents our test code.

LISTING 31.8

The testAddGetStudent Test Code

@Test
public void testAddGetStudent() throws Exception {
    // Create our method test inputs
    String studentId = "0001";
    Student student = new Student(studentId);
    student.setFirstName("Ada");
    student.setLastName("Lovelace");
    student.setCohort("Java-May-1845");
 
    //  Add the student to the DAO
    testDao.addStudent(studentId, student);
    // Get the student from the DAO
    Student retrievedStudent = testDao.getStudent(studentId);
 
    // Check that the data is equal
    assertEquals(student.getStudentId(),
                retrievedStudent.getStudentId(),
                "Checking student id.");
    assertEquals(student.getFirstName(),
                retrievedStudent.getFirstName(),
                "Checking student first name.");
    assertEquals(student.getLastName(), 
                retrievedStudent.getLastName(),
                "Checking student last name.");
    assertEquals(student.getCohort(), 
                retrievedStudent.getCohort(),
                "Checking student cohort.");
}

Our first test is now complete, but we've only touched two of our DAO methods, and we only used the test with one Student object. Let's do another.

Get All Students

Let's look at the DAO interface method and comment before we get started on designing a test for the getAllStudents method. This is shown in Listing 31.9.

LISTING 31.9

The getAllStudents Interface Method and Comments

/**
 * Returns a List of all Students on the roster.
 *
 * @return Student List containing all students on the roster.
 */
List<Student> getAllStudents()
     throws ClassRosterPersistenceException;

This is an easier method to consider. It takes in no parameters and simply returns a list of students within the DAO. To properly test this method, we're going to have to use it alongside addStudent and maybe removeStudent to change what is contained within the list.

ClassRosterDaoTest: Adding and Getting All Students

This test is slightly more complicated than the previous Add/Get test, but not by much. Here we are focusing on testing the two methods addStudent and getAllStudents and verifying that each method works. To do this properly, we should really use multiple Student objects. In this test, we do the following:

  1. Create and add two Student objects to the DAO (Arrange).
  2. Get all the Student objects from the DAO (Act).
  3. Check to see that the DAO returned the two objects (Assert).

Listing 31.10 shows the resulting test code.

LISTING 31.10

The testAllGetAllStudents Test Code

@Test
public void testAddGetAllStudents() throws Exception {
    // Create our first student
    Student firstStudent = new Student("0001");
    firstStudent.setFirstName("Ada");
    firstStudent.setLastName("Lovelace");
    firstStudent.setCohort("Java-May-1845");
 
    // Create our second student
    Student secondStudent = new Student("0002");
    secondStudent.setFirstName("Charles");
    secondStudent.setLastName("Babbage");
    secondStudent.setCohort(".NET-May-1845");
 
    // Add both our students to the DAO
    testDao.addStudent(firstStudent.getStudentId(), firstStudent);
    testDao.addStudent(secondStudent.getStudentId(), secondStudent);
 
    // Retrieve the list of all students within the DAO
    List<Student> allStudents = testDao.getAllStudents();
 
    // First check the general contents of the list
    assertNotNull(allStudents, "The list of students must not be null.");
    assertEquals(2, allStudents.size(),"The list of students should have 2 students.");
 
    // Then the specifics
    assertTrue(testDao.getAllStudents().contains(firstStudent),
                "The list of students should include Ada.");
    assertTrue(testDao.getAllStudents().contains(secondStudent),
            "The list of students should include Charles.");
 
}

Remove Student

Let's look at the DAO interface method and comment for removing a student before we get started on designing a test for this method. Listing 31.11 shows the removeStudent interface method.

LISTING 31.11

The removeStudent Interface Method and Comments

/**
 * Removes from the roster the student associated with the given id.
 * Returns the student object that is being removed or null if
 * there is no student associated with the given id
 *
 * @param studentId id of student to be removed
 * @return Student object that was removed or null if no student
 * was associated with the given student id
 */
Student removeStudent(String studentId);

We know the following things about removeStudent:

  • To call this method, we will need a studentId String.
  • The return should be a Student object. In theory, this is a student that was previously stored under that ID.
  • If there was no student in the DAO with that studentId, the return should be null.
  • The state effect of this on the DAO is that the returned student should no longer be within the DAO.
  • There is another mention of a ClassRosterPersistenceException, which implies more persistent data storage.

ClassRosterDaoTest: Adding and Removing Students

In this test, we do the following:

  1. Create and add two Student objects to the DAO (Arrange).
  2. Remove one of the students from the DAO (Act).
  3. Check to see that there is only one student left in the DAO (Assert).
  4. Check to see that the DAO returns null if we try to retrieve the removed student (Assert).
  5. Remove the other student from the DAO (Act).
  6. Check to see that there are no students in the DAO (Assert).
  7. Check to see that the DAO returns null if we try to retrieve the removed student (Assert).

Even though this is only one test, as shown in Listing 31.12, we are acting and asserting several times. This is perfectly fine. You can act and assert as many times as needed to fully execute the test case.

LISTING 31.12

The testRemoveStudent Test Code

@Test
public void testRemoveStudent() throws Exception {
    // Create two new students
    Student firstStudent = new Student("0001");
    firstStudent.setFirstName("Ada");
    firstStudent.setLastName("Lovelace");
    firstStudent.setCohort("Java-May-1945");
 
    Student secondStudent = new Student("0002");
    secondStudent.setFirstName("Charles");
    secondStudent.setLastName("Babbage");
    secondStudent.setCohort(".NET-May-1945");
 
    // Add both to the DAO
    testDao.addStudent(firstStudent.getStudentId(), firstStudent);
    testDao.addStudent(secondStudent.getStudentId(), secondStudent);
 
    // remove the first student - Ada
    Student removedStudent = testDao.removeStudent(firstStudent.getStudentId());
 
    // Check that the correct object was removed.
    assertEquals(removedStudent, firstStudent, "The removed student should be Ada.");
 
    // Getall the students
    List<Student> allStudents = testDao.getAllStudents();
 
    // First check the general contents of the list
    assertNotNull( allStudents, "All students list should be not null.");
    assertEquals( 1, allStudents.size(), "All students should only have 1 student.");
 
    // Then the specifics
    assertFalse( allStudents.contains(firstStudent), 
                 "All students should NOT include Ada.");
    assertTrue( allStudents.contains(secondStudent), 
                "All students should NOT include Charles.");    
 
    // Remove the second student
    removedStudent = testDao.removeStudent(secondStudent.getStudentId());
    // Check that the correct object was removed.
    assertEquals( removedStudent, secondStudent, 
                  "The removed student should be Charles.");
 
    // retrieve all of the students again and check the list.
    allStudents = testDao.getAllStudents();
 
    // Check the contents of the list - it should be empty
    assertTrue( allStudents.isEmpty(), 
                "The retrieved list of students should be empty.");
 
    // Try to get both students by their old id. They should be null.
    Student retrievedStudent = testDao.getStudent(firstStudent.getStudentId());
    assertNull(retrievedStudent, "Ada was removed, should be null.");
 
    retrievedStudent = testDao.getStudent(secondStudent.getStudentId());
    assertNull(retrievedStudent, "Charles was removed, should be null.");
 
}

UNIT TESTING THE SERVICE LAYER

In this part of the lesson, we will unit test the service layer for the Class Roster application. As mentioned at the beginning of this lesson, we will use a new testing technique called stubbing.

Planning the Test Design

The main purpose of the unit test suite for the service layer is to test that the business rules are being applied properly. For completeness, we will also test the pass-through methods of the service layer. We will test these business rules:

  • Creation of a Student object with an existing student ID is prohibited.
  • All fields on the Student object must have nonempty values.

These business rules are enforced in the createStudent method of the DAO. We will need three test cases:

  • Create valid student. All fields have values, and the student does not have an existing student ID.
  • Create a student with an existing student ID.
  • Create a student with one or more empty field values.

We will also implement tests for getAllStudents, getStudent, and removeStudent. These tests will be similar to the tests we created for the matching DAO methods in a previous step.

Creating the Test Class

You should remember this step from our DAOImpl testing step. We will create a JUnit test class that will hold our test suite for our service layer. Do the following:

  1. In the NetBeans Project view, right-click the ServiceLayerImpl class.
  2. Select Tools, and then select Create/Update Tests.
  3. Walk through the wizard to create a new JUnit class.
    • The name should look similar to classroster.service.ClassRosterServiceLayerImplTest.
    • Ensure that Test Packages is selected in the Location drop-down.
    • Generated Code Checkbox List should have everything selected.
    • Method Access Levels Checkbox List should have nothing selected.
    • Generated Comments Checkbox List should have nothing selected.
  4. Once these steps are finished, click the OK button.

Completing the wizard should leave you with a new Java test class, as shown in Listing 31.13, where we will write our service layer test suite.

LISTING 31.13

ClassRosterServiceLayerImplTest

public class ClassRosterServiceLayerImplTest {
 
    public ClassRosterServiceLayerImplTest() {
    }
 
    @BeforeAll
    public static void setUpClass() {
    }
 
    @AfterAll
    public static void tearDownClass() {
    }
 
    @BeforeEach
    public void setUp() {
    }
 
    @AfterEach
    public void tearDown() {
    }
 
    @Test
    public void testSomeMethod() {
        fail("The test case is a prototype.");
    }
}

Creating the DAO Stubs

The service layer is not responsible for storing or retrieving Student objects. That is the job of the DAO. This means we don't have to (or want to) test the actual persistence of Student objects in this test suite. We only want to test the business rules and the integration between the service layer and the DAO.

Given this fact, we can use stubbed versions of our DAOs to test the functionality of the service layer. Since we are programming to interfaces and using constructor-based dependency injection, it will be easy to use the stubbed versions of the DAOs instead of the file-based implementations.

A stubbed version of a component simply returns canned data for any method call. We can set up a stubbed version of a component to act just about any way we want or need it to. Let's take a look at the stubbed implementation of our DAOs.

ClassRosterAuditDaoStubImpl

The ClassRosterAuditDaoStubImpl implementation shown in Listing 31.14 is simple. The writeAuditEntry method does nothing. This allows the service layer to make the call to the Audit DAO, but nothing will get written to the audit log file.

This is a testing class and belongs inside our test packages, not our development sources. Therefore, to create this testing stub, proceed with the following steps:

  1. In the NetBeans Project view, right-click the ClassRosterServiceLayerImplTest's test package.
  2. Select New and then Java Class.
  3. Name it ClassRosterAuditDaoStubImpl.
  4. Click the Finish button.
  5. Update the internal code to the shell implementation in Listing 31.14.

LISTING 31.14

The ClassRosterAuditDaoStubImpl Shell Implementation

public class ClassRosterAuditDaoStubImpl implements ClassRosterAuditDao {
 
    @Override
    public void writeAuditEntry(String entry) throws ClassRosterPersistenceException {
        //do nothing . . .
    }
}

ClassRosterDaoStubImpl

This ClassRosterDaoStubImpl implementation is a bit more complicated than the Audit DAO stub implementation, but it is still quite straightforward. The stub implementation has the following features:

  • A member field of type Student

    This represents the one and only student in the DAOStub.

  • Constructor

    We have two. One is a no-arg constructor that instantiates a hard-coded student for our stub. The other allows a test student to be injected via the constructor by a test class.

  • addStudent

    This returns our onlyStudent field if the ID matches our onlyStudent's ID. Otherwise, it returns null. Note that there is no persistence. The incoming Student parameter is never added to the DAOStub or persisted in any way.

  • getAllStudents

    This method simply returns a List containing the one and only student.

  • getStudent

    This returns our onlyStudent field if the ID matches our onlyStudent's ID; otherwise, it returns null.

  • removeStudent

    This returns our onlyStudent field if the ID matches our onlyStudent's ID; otherwise, it returns null. Note that this does not change or remove our only Studentexistence within our DAOStub.

To create this stub, follow similar steps as the AuditDaoStub previously, and then fill out your ClassRosterDaoStubImpl with the code in Listing 31.15.

LISTING 31.15

ClassRosterDaoStubImpl

public class ClassRosterDaoStubImpl implements ClassRosterDao {
 
    public Student onlyStudent;
 
    public ClassRosterDaoStubImpl() {
    onlyStudent = new Student("0001");
        onlyStudent.setFirstName("Ada");
        onlyStudent.setLastName("Lovelace");
        onlyStudent.setCohort("Java-May-1845");
    }
 
    public ClassRosterDaoStubImpl(Student testStudent){
         this.onlyStudent = testStudent;
     }
 
    @Override
    public Student addStudent(String studentId, Student student)
                  throws ClassRosterPersistenceException {
        if (studentId.equals(onlyStudent.getStudentId())) {
            return onlyStudent;
        } else {
            return null;
        }
    }
 
    @Override
    public List<Student> getAllStudents()
                 throws ClassRosterPersistenceException {
        List<Student> studentList = new ArrayList<>();
        studentList.add(onlyStudent);
        return studentList;
    }
 
    @Override
    public Student getStudent(String studentId)
                throws ClassRosterPersistenceException {
        if (studentId.equals(onlyStudent.getStudentId())) {
            return onlyStudent;
        } else {
            return null;
        }       
    }
 
    @Override
    public Student removeStudent(String studentId)
                throws ClassRosterPersistenceException {
        if (studentId.equals(onlyStudent.getStudentId())) {
            return onlyStudent;
        } else {
            return null;
        }
    }   
}

Test Setup

By using stubbed DAOs for these tests, we have essentially defined the state of our test system. Since the DAOs are the properties that belong to our service layer, their state represents the state of our service layer. That means if we can start these DAOs in a good state, so will our service layer. Since our stubs start in a predefined state, we will begin testing our service layer with the assumption that it contains a Class Roster DAO with exactly one student.

Although we don't have to do any work to put our code in a known good state, we do have to create the service layer object and wire in our stub DAOs. This code is similar to the code we have in the App class of the Class Roster application. Update the top of your ClassRosterServiceLayerImplTest with the setup in Listing 31.16.

LISTING 31.16

Create the Service Layer Object and Wire a Stub DAO

private ClassRosterServiceLayer service;
 
public ClassRosterServiceLayerImplTest() {
    ClassRosterDao dao = new ClassRosterDaoStubImpl();
    ClassRosterAuditDao auditDao = new ClassRosterAuditDaoStubImpl();
 
    service = new ClassRosterServiceLayerImpl(dao, auditDao);
}

Test Implementation

Finally, we will look at our test implementations. These include testCreateValidStudent, testCreateStudentDuplicateId, testGetAllStudents, testGetStudent, and testRemoveStudent.

testCreateValidStudent

The testCreateValidStudent test shown in Listing 31.17 is quite straightforward. We are simply asserting that the creation of a valid student (no duplicate student ID and values for all fields that the only hard-coded student in our DaoStub) does not cause an exception to be thrown.

LISTING 31.17

The testCreateValidStudent test

@Test
public void testCreateValidStudent() {

    // ARRANGE
    Student student = new Student("0002");
    student.setFirstName("Charles");
    student.setLastName("Babbage");
    student.setCohort(".NET-May-1845");
    // ACT
    try {
        service.createStudent(student);
    } catch (ClassRosterDuplicateIdException
        | ClassRosterDataValidationException
            | ClassRosterPersistenceException e) {
    // ASSERT
        fail("Student was valid. No exception should have been thrown.");
    }
}

testCreateStudentDuplicateId

The testCreateStudentDuplicateID test shown in Listing 31.18 asserts that a ClassRosterDuplicateIdException is thrown when trying to create a student with an existing student ID. In this case, we know that the stubbed implementation of the Class Roster DAO has an existing student with an ID of 0001 so we attempt to create a new student with that student ID. Since we expect an exception to be thrown by this call, we surround it with a try/catch.

  • If the call executes and no exception is thrown, we fail the test with a message saying we expected an exception to be thrown.
  • If the expected exception is thrown, we simply return. Since there are no errors or exceptions, this lets the JUnit test framework know that this test passed.
  • If a different exception is thrown, the test will fail.

LISTING 31.18

The testCreateStudentDuplicateId Test

@Test
public void testCreateDuplicateIdStudent() {
    // ARRANGE
    Student student = new Student("0001");
    student.setFirstName("Charles");
    student.setLastName("Babbage");
    student.setCohort(".NET-May-1845");
 
    // ACT
    try {
        service.createStudent(student);
        fail("Expected DupeId Exception was not thrown.");
    } catch (ClassRosterDataValidationException
            | ClassRosterPersistenceException e) {
    // ASSERT
        fail("Incorrect exception was thrown.");
    } catch (ClassRosterDuplicateIdException e){
        return;
    }
}

testCreateStudentInvalidData

The testCreateStudentInvalidData test shown in Listing 31.19 is similar to the test for duplicate student ID. Here we ensure that we don't have a duplicate student ID and then also leave one of the fields blank. We use the same try/catch techniques as shown in the previous example except we are looking for a different exception.

LISTING 31.19

The testCreateStudentInvalidData Test

@Test
public void testCreateStudentInvalidData() throws Exception {
    // ARRANGE
    Student student = new Student("0002");
    student.setFirstName("");
    student.setLastName("Babbage");
    student.setCohort(".NET-May-1845");
 
    // ACT
    try {
        service.createStudent(student);
        fail("Expected ValidationException was not thrown.");
    } catch (ClassRosterDuplicateIdException
            | ClassRosterPersistenceException e) {
    // ASSERT
        fail("Incorrect exception was thrown.");
    } catch (ClassRosterDataValidationException e){
        return;
    }  
}

testGetAllStudents

In the testGetAllStudents test shown in Listing 31.20, since we know that the stubbed Class Roster DAO contains only one student, we assert that only one student is returned from the getAllStudents service layer method.

LISTING 31.20

The testGetAllStudents Test

@Test
public void testGetAllStudents() throws Exception {
    // ARRANGE
    Student testClone = new Student("0001");
        testClone.setFirstName("Ada");
        testClone.setLastName("Lovelace");
        testClone.setCohort("Java-May-1845");
 
    // ACT & ASSERT
    assertEquals( 1, service.getAllStudents().size(), 
                                   "Should only have one student.");
    assertTrue( service.getAllStudents().contains(testClone),
                              "The one student should be Ada.");
}

testGetStudent

For the testGetStudent test shown in Listing 31.21, since we know that the stubbed Class Roster DAO only contains one Student with Student ID = 0001, we assert that a Student is returned when we ask for Student ID 0001 and that no student is returned when we ask for Student ID 0042.

LISTING 31.21

The testGetStudent Test

@Test
public void testGetStudent() throws Exception {
    // ARRANGE
    Student testClone = new Student("0001");
        testClone.setFirstName("Ada");
        testClone.setLastName("Lovelace");
        testClone.setCohort("Java-May-1845");
 
    // ACT & ASSERT
    Student shouldBeAda = service.getStudent("0001");
    assertNotNull(shouldBeAda, "Getting 0001 should be not null.");
    assertEquals( testClone, shouldBeAda,
                             "Student stored under 0001 should be Ada.");
 
    Student shouldBeNull = service.getStudent("0042");    
    assertNull( shouldBeNull, "Getting 0042 should be null.");
 
}

testRemoveStudent

The behavior of the removeStudent method is that it will remove the student and return the associated Student object if a student exists for the given student ID. Otherwise, it will do nothing and return null. In the testRemoveStudent test shown in Listing 31.22, we assert that a Student object is returned when we remove student ID 0001 and that null is returned when we remove student ID 0042.

LISTING 31.22

The testRemoveStudent Test

@Test
public void testRemoveStudent() throws Exception {
    // ARRANGE
    Student testClone = new Student("0001");
        testClone.setFirstName("Ada");
        testClone.setLastName("Lovelace");
        testClone.setCohort("Java-May-1845");
 
    // ACT & ASSERT
    Student shouldBeAda = service.removeStudent("0001");
    assertNotNull( shouldBeAda, "Removing 0001 should be not null.");
    assertEquals( testClone, shouldBeAda, "Student removed from 0001 should be Ada.");
 
    Student shouldBeNull = service.removeStudent("0042");    
    assertNull( shouldBeNull, "Removing 0042 should be null.");
 
}

SUMMARY

In this lesson, we discussed and demonstrated the techniques for unit testing stateful code. We then completed the unit tests for the Class Roster service layer and saw how stubbed implementations can be used to test other components. The following are the main points to remember from this lesson:

  • You must put stateful code into a known good state before unit testing it.
  • You must be mindful of the order in which you test and call methods for stateful code. Calls to one method can affect the results from other methods.
  • JUnit provides set up and tear down methods to help put stateful code into a known good state.
  • Implementing the equals and hashCode methods on your DTOs can make unit testing a lot easier. Remember that you must use the same fields to calculate equality and the hash code for your objects.
  • Stubbed implementations of components such as DAOs can be used to test components that use them.
  • Hard-coded stubbed component implementations have a fixed state.
  • When testing components (such as the service layer) that depend on other components (such as DAOs), you must wire the dependencies together as part of your overall test setup.
  • The combination of a try/catch block and the JUnit static method called fail can be used to test conditions where an exception is expected.

EXERCISES

Many people learn best by doing, so this section includes exercises using what you learned in this lesson and previously in this course.

  • Exercise 1: Testing the Address Book app
  • Exercise 2: Testing the DVD Library program

Exercise 1: Testing the Address Book App

Design and implement a complete set of unit tests for the DAO of the Address Book application that you created in Lesson 27. Use the lesson notes as a guide and pattern your test suite after the test suite created in the Class Roster unit test code-along earlier in this lesson.

Exercise 2: Testing the DVD Library

Design and implement a complete set of unit tests for the DAO of the DVD Library application that you created in Lesson 27. Use the information from this lesson as a guide and pattern your test suite after the test suite created in the Class Roster application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.27.232