I'll assume setDate()
looks something like this:
public void setDate(Date d) {
if (!isValidDate(d))
throw SomeException(...);
this.date = d;
}
public Date getDate() { return this.date; }
I'm including a getDate()
as well since tests for setting and getting can't be separated.
There are two approaches to handle the test duplication.
Only test what the method does directly.
There are two schools of though regarding unit tests:
- The test should describe the full behaviour of the unit under test.
- The test should only cover the value added by the unit under test, and ignore work done by external parts.
Using the latter approach, what does setDate()
do? For invalid dates (determined with an external methods that's not going to be tested here), it throws an exception. Otherwise, we can get the same date back with getDate()
.
This gives us exactly two test cases. In a sketch:
void test_setDate__throwsOnInvalidDates() {
MyObject obj = new MyObject();
Date invalidDate = ...;
try {
obj.setDate(invalidDate);
assert(false, "setDate() did not reject the invalid date");
} catch (SomeException e) {
assert(true);
}
}
void test_getDate__canRetrieveValuesFromSetDate() {
MyObject obj = new MyObject();
Date d = new Date(...);
obj.setDate(d);
assertEquals(obj.getDate(), d);
}
Generate test data once, use for both tests.
By storing a list of valid and invalid dates, we can test both isValidDate()
and setDate()
without notable repetition. How you can parametrize your tests to use a list of cases depends on your framework, here I'll put the loop inside a test:
Date[] validDates = { ... };
Date[] invalidDates = { ... };
void test_isValidDate__acceptsValidDates() {
for (Date d : validDates)
assertTrue(isValidDate(d), d.toString());
}
void test_isValidDate__rejectsInvalidDates() {
for (Date d : invalidDates)
assertTrue(!sValidDate(d), d.toString());
}
...
void test_getDate__canRetrieveValuesFromSetDate() {
for (Date d : validDates) {
MyObject obj = new MyObject();
obj.setDate(d);
assertEquals(obj.getDate(), d);
}
}
void test_setDate__throwsOnInvalidDates() {
for (Date d : invalidDates) {
MyObject obj = ...;
try {
obj.setDate(d);
assertTrue(false, ...);
} catch (SomeException e) {
assertTrue(true);
}
}
}
In practice, these should be separate test cases instead of loops within a single test case, so that a failing test does not prevent the other dates from being tested – more data on which dates fail or succeed can make debugging much easier.
While this approach is immune to refactoring, it does cause tests to run longer (an issue for very large test suites), and creates the problem of generating the necessary data.
I personally prefer the first approach – only testing the function added immediately by some method. This helps to keep test suites small and meaningful. But if you feel that is not sufficient, or if you expect that a programmer would carelessly remove a validation check, then generating a reusable list of test data is probably better. I have used both approaches, and both work well.