Guided Task: Test Driven Development
In this section, you will start creating tests for the implementation of the Conflict
interface in Activity
.
Learning Outcomes
- Create a test class
- Implement a test for an exception
Best Practice: Test Driven Development (TDD)
Test Driven Development (TDD) is the process of using the requirements and object design to write the unit tests for a class first, and then use the tests to drive the implementation of the methods in the class. The process is typically iterative. Start with the simplest test first and implement the functionality to pass. Then add additional tests that consider other paths, edge cases, and error cases. After adding each test, implement the functionality to pass it (if needed).
TDD is a highly effective process for developing code that is well tested. By testing right away and letting the tests drive development, you will implement more high quality code more quickly and not run out of time for testing!
Implement the Conflict
Interface in Activity
Since Activity
contains the fields related to the meetingDays
, startTime
, and endTime
, you can implement the Conflict.checkConflict()
functionality in the Activity
class. If the child classes need custom conflict implementation later, they can override the method.
You can use your tests to drive the creation of new methods in your code as in using Quick Fixes for Error-Driven Development. However, this may not be appropriate in all cases - Quick Fix will help you create the checkConflict()
method, but will not know that the method is part of implementing the Conflict
interface. Instead, you’ll set up the appropriate structure and generate a test class from the structure.
Follow these steps to set up Activity
for testing the Conflict
functionality:
- Open
Activity
in the editor. - Update the
Activity
class header to includeimplements Conflict
. When complete, the header should look likepublic abstract class Activity implements Conflict {
. - Because
Activity
is an abstract class, there are no compiler errors aboutActivity
needing an implementation ofConflict
’s method. Instead,Course
andEvent
won’t compile. However, you wantActivity
to contain thecheckConflict()
implementation. You can use Eclipse’s source generation tool to implement the method inActivity
by right clicking in the editor and selecting Source > Override/Implement Methods. Check the option forConflict.checkConflict(Activity)
and click OK.
Now that Activity
implements the Conflict.checkConflict()
method, the compiler errors in Course
and Event
are resolved.
Reminder: Creating a JUnit Test Class from a Class Under Test
Guided Task: Implement and Test ConflictException
- Creating a Test Class.
Create ActivityTest
for Testing Conflict Functionality.
Now, create the JUnit class to test the Activity.checkConflict()
implementation. Create a new JUnit test for Activity
named ActivityTest
in the edu.ncsu.csc216.wolf_scheduler.course
package of the test/
folder in WolfScheduler
. Since you’ve been provided test classes that test the other methods, you should only generate a test method for the checkConflict()
method. After generating the test, you can remove the fail()
statement since you are about to implement the test.
checkConflict()
Overview
The checkConflict()
method is an instance method of Activity
. That means it compares the current instance (i.e., this
) with the parameter possibleConflictingActivity
to see if there is any conflict in their days and times. If there is a conflict, the checked ConflictException
is thrown. If there is no conflict, nothing happens and the statement in the client code following the call to checkConflict()
is executed.
Two Activity
objects are in conflict if there is at least one day with an overlapping time. A time is overlapping if the minute is the same. For example, an Activity
on Monday from 1:30PM-2:45PM would conflict with an Activity
on Monday from 2:45PM-3:30PM because of the overlap on the 2:45 minute. You’ll encode this test shortly.
Start by Javadocing your checkConflict()
method to describe how the conflict check works in the context of the Activity
hierarchy.
Writing Tests
The following sections will detail the tests that you should write to fully test the checkConflict()
method.
No Conflict Test
Since you are testing a method that may throw a checked exception, the test code must be surrounded with a try/catch
block. For a no conflict test, the expected result is that the exception is NOT thrown, so the initial test structure is:
try {
//Call to checkConflict
} catch (ConflictException e) {
fail(); //If an exception is thrown when there is no conflict the test fails.
}
The rest of the test is creating the two Activity
objects to check for conflict, and then to check the conflict. Since Activity
is abstract
, you cannot directly construct Activity
. Instead, you must construct a Course
or Event
object for the comparison.
Activity a1 = new Course("CSC 216", "Software Development Fundamentals", "001", 3, "sesmith5", "MW", 1330, 1445);
Activity a2 = new Course("CSC 216", "Software Development Fundamentals", "001", 3, "sesmith5", "TH", 1330, 1445);
Next, the checkConflict()
method is called on one of the objects. The other is passed as a parameter. Since the call could cause an exception, the call to checkConflict()
is placed in the try
block.
Finally, there should be some type of assert in the body of the try
block. It’s not enough that no exception was thrown. Instead, you need to check that the call to the checkConflict()
method did NOT lead to a change of any of the state of the Activity
object related to the meetingDays
, startTime
, and endTime
for either object. You can check all three at once by calling the getMeetingString()
method.
The final test looks like the following:
@Test
public void testCheckConflict() {
Activity a1 = new Course("CSC 216", "Software Development Fundamentals", "001", 3, "sesmith5", "MW", 1330, 1445);
Activity a2 = new Course("CSC 216", "Software Development Fundamentals", "001", 3, "sesmith5", "TH", 1330, 1445);
try {
a1.checkConflict(a2);
assertEquals("Incorrect meeting string for this Activity.", "MW 1:30PM-2:45PM", a1.getMeetingString());
assertEquals("Incorrect meeting string for possibleConflictingActivity.", "TH 1:30PM-2:45PM", a2.getMeetingString());
} catch (ConflictException e) {
fail("A ConflictException was thrown when two Activities at the same time on completely distinct days were compared.");
}
}
Run the test. Since you have not yet implemented checkConflict()
, the test should pass!!!? For now, that’s ok. We will shortly be implementing the checkConflict()
method once we have a failing test to pass. However, our happy path test is an important test to ensure that as you implement the rest of checkConflict()
that you do not start finding a conflict where there should be no conflict!
Testing an Exception - Conflict Test
The next test type to consider is a test when there is a conflict. In this case, the expected results are that the ConflictException
is thrown. Continuing execution without an exception is a failing test. The initial test structure is:
try {
//Call to checkConflict
fail(); //If the test reaches this point it fails since the exception was NOT thrown
} catch (ConflictException e) {
//Check that no state was changed
}
We can continue the test in the same test method as above, or we can create a new test method to consider this specific case. By continuing with the previous test method, we can work with the existing local variables a1
and a2
and modify the days or times to lead to a conflict. By writing another test method, we can more clearly identify which functionality is passing or failing. The example below is a separate test method, but you are welcome to continue the implementation in the other test method.
@Test
public void testCheckConflictWithConflict() {
Activity a1 = new Course("CSC 216", "Software Development Fundamentals", "001", 3, "sesmith5", "MW", 1330, 1445);
Activity a2 = new Course("CSC 216", "Software Development Fundamentals", "001", 3, "sesmith5", "M", 1330, 1445);
try {
a1.checkConflict(a2);
fail("A ConflictException was NOT thrown when two Activities had a day/time conflict.");
} catch (ConflictException e) {
assertEquals("Incorrect meeting string for this Activity.", "MW 1:30PM-2:45PM", a1.getMeetingString());
assertEquals("Incorrect meeting string for possibleConflictingActivity.", "M 1:30PM-2:45PM", a2.getMeetingString());
}
}
Run the test. It fails! But that is exactly what you want. It means you have a test that you can use to start driving the implementation of the checkConflict()
functionality in Activity
, which you’ll work on in the next step.
Testing Behaviors to Avoid
There are behaviors that you want to avoid when testing. PMD will flag some of these behaviors with notifications (and a PMD notification leads to a loss of a point AND PMD testing notifications will block the teaching staff tests from running on your code!)
Test Method with No assert*()
Every test method should have at least one assert*()
method call. The goal of a test method is to assert or check something about the code under test to verify the code is working correctly. It’s very tempting to leave out an assert*()
method call because your test method will pass (and you may also achieve coverage). But that means you can have no confidence in your code! Always write a test method to assert something about the code under test!
Meaningless assert*()
Statements
A meaningless assert*()
statement is something like assertTrue(true);
, assertFalse(false);
, or assertNull(null);
. There is ALWAYS something to check about the code under test. At a minimum you can check that the state has not changed or that the type is correct. Avoid meaningless assert statements. If you need help identifying what you should check, contact the teaching staff.
Reference: Staging and Pushing to GitHub
GitHub Resources:
Check Your Progress
Complete the following tasks before pushing your work to GitHub.
- Make sure that all fields, methods, and constructors are commented.
- Resolve all static analysis notifications.
- Fix test failures.
- Commit and push your code changes with a meaningful commit message. Label your commit with “[Test]” for future you!
- Check Jenkins results for a yellow ball! At this point, your code should compile against the teaching staff tests.