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.
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 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.
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
.
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.
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 Student
s’ 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
:
Student
properties are important for this.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.
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.
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.
Example of Student's New toString Method
@Override
public String toString() {
return "Student{" + "firstName=" + firstName + ", lastName=" + lastName + ", studentId=" + studentId + ", cohort=" + cohort + '}';
}
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.
ClassRosterDaoFileImpl
class.classroster.dao.ClassRosterDaoFileImplTest
.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.
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 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
tearDownClass
annotated with @AfterAll
setUp
annotated with @BeforeEach
tearDown
annotated with @AfterEach
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.
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.
We must add two general steps to the Arrange part of the unit testing process for stateful code.
setUp
method.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:
Our ClassRosterDao
has four methods that we must test.
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.
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.
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
:
studentId String
and a Student
object.ClassRosterDaoFileImplTest
and be associated with the given studentId
.studentId
, the return should be null
.Student
associated will be returned.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.
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
:
studentId String
.Student
object. In theory, this is a student previously stored under that ID.studentId
, the return should be null
.ClassRosterPersistenceException
, which again implies touching a persistent data store.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.
Student
object (Arrange).Listing 31.8 presents our test code.
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.
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.
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.
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:
Student
objects to the DAO (Arrange).Student
objects from the DAO (Act).Listing 31.10 shows the resulting test code.
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.");
}
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.
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
:
studentId String
.Student
object. In theory, this is a student that was previously stored under that ID.studentId
, the return should be null
.ClassRosterPersistenceException
, which implies more persistent data storage.In this test, we do the following:
Student
objects to the DAO (Arrange).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.
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.");
}
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.
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:
Student
object with an existing student ID is prohibited.Student
object must have nonempty values.These business rules are enforced in the createStudent
method of the DAO. We will need three test cases:
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.
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:
ServiceLayerImpl
class.classroster.service.ClassRosterServiceLayerImplTest
.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.
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.");
}
}
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.
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:
ClassRosterServiceLayerImplTest
's test package.The ClassRosterAuditDaoStubImpl Shell Implementation
public class ClassRosterAuditDaoStubImpl implements ClassRosterAuditDao {
@Override
public void writeAuditEntry(String entry) throws ClassRosterPersistenceException {
//do nothing . . .
}
}
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:
Student
This represents the one and only student in the DAOStub
.
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.
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;
}
}
}
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.
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);
}
Finally, we will look at our test implementations. These include testCreateValidStudent
, testCreateStudentDuplicateId
, testGetAllStudents
, testGetStudent
, and testRemoveStudent
.
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.
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.");
}
}
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
.
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;
}
}
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.
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;
}
}
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.
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.");
}
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.
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.");
}
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.
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.");
}
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:
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.try/catch
block and the JUnit static method called fail
can be used to test conditions where an exception is expected.Many people learn best by doing, so this section includes exercises using what you learned in this lesson and previously in this course.
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.
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.
3.15.27.232