Sunday, August 23, 2009

Inheriting Tests with an Abstract Test Case

I was wondering what might be a good way to write unit tests for defined methods in an abstract class. I then tried out using an abstract test class after perusing the JUnit FAQ.

An abstract test class can be used to help ensure that the subclasses of the abstract class behave as expected.

I decided to take a different approach then what the article linked from the FAQ took. With an abstract source class, I thought I would create an abstract class with concrete tests used to test the non-abstract methods defined in the abstract source class.

The test classes for extensions of the abstract test class would then inherit the tests for the methods defined in the abstract source class. If extensions of that class could override the implementation of those methods, but the tests would be there to ensure the overriding methods produce the same result as the overridden method.

Take the following example (which is contrived for this example, and in no way endorses any OO principles). An abstract class Detabber has one concrete method that replaces all tabs with spaces for whatever it is given:

abstract class Detabber {
abstract def detabEntity(entity)
def detab(text) { text.replaceAll("\t", " ") }
}

All implementations are charged with providing what is to be de-tabbed, here are two:

class FileDetabber extends Detabber {
@Override def detabEntity(entity) {
def result
new File(entity).getText().eachLine { line -> result += detab(line) }
result
}
}

class ResourceDetabber extends Detabber {
@Override def detabEntity(entity) {
def text = getClass().getClassLoader().getResourceAsStream(entity).getText()
detab(text)
}
}

Next, I write my abstract test class for the abstract class Detabber:

abstract class DetabberTest {
Detabber detabber
@Test void test_detab() {
assertEquals "string was detabbed", "a b", detabber.detab("a\tb")
}
}

In the test phase, there should be no tests to be run currently:

There are no tests to run.

Results :
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0

Here I add a couple extensions of the above test class, but add no new tests. Each class should inherit the defined test from the extended class:

// tests for the implementation of detabEntity(entity) omitted for better illustration
class FileDetabberTest extends DetabberTest {
@Before void setUp() { detabber = new FileDetabber() }
}

class ResourceDetabberTest extends DetabberTest {
@Before void setUp() { detabber = new ResourceDetabber() }
}

Now two tests should be run:

Running ResourceDetabberTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.271 sec
Running FileDetabberTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.026 sec

Results :
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

It might look like the inherited test add no value, besides upping my the number of tests I can say run in a report. I could just define the same test, in either FileDetabber or ResourceDetabber and be done with it, covering the method once since it never changes.

For arguments sake, let's say a new Detabber comes along that, maybe for efficiency's sake, wants to override Detabber.detab(text):

class FasterResourceDetabber extends ResourceDetabber {
@Override def detab(text) {
def matcher = text =~ "\t"
matcher.replaceAll(" ") // actually faster? I don't know
}
}

It's test class can still inherit the test from DetabberTest and avoid having to have its own test (or tests if more were present in DetabberTest) without having to add a new code:

class FasterResourceDetabberTest extends DetabberTest {
@Before void setUp() { detabber = new FasterResourceDetabber() }
}

Three tests are now run:

Running FasterResourceDetabberTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.35 sec
Running ResourceDetabberTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.042 sec
Running FileDetabberTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.016 sec

Results :
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

Here we verify the new implementation of detab(text) behaves as one might expect. The same input functionally produces the same output as before with the new implementation.

Earlier, I omitted tests for the implementations of detabEntity(entity) in FileDetabber and ResourceDetabber, so I'll add them here with all the written code as a summary:

Source:

abstract class Detabber {
abstract def detabEntity(entity)

def detab(text) {
text.replaceAll("\t", " ")
}
}

class FileDetabber extends Detabber {
@Override def detabEntity(entity) {
def result = ""
new File(entity).getText().eachLine { line ->
result += detab(line)
}
result
}
}

class ResourceDetabber extends Detabber {
@Override def detabEntity(entity) {
def text = getClass().getClassLoader().getResourceAsStream(entity).getText()
detab(text)
}
}

class FasterResourceDetabber extends ResourceDetabber {
@Override def detab(text) {
def matcher = text =~ "\t"
matcher.replaceAll(" ")
}
}

Test Source:

import org.junit.Before
import org.junit.Test
import static org.junit.Assert.*

abstract class DetabberTest {
Detabber detabber
@Test void test_detab() {
assertEquals "string was detabbed", "a b", detabber.detab("a\tb")
}
}

class FileDetabberTest extends DetabberTest {

@Before void setUp() {
detabber = new FileDetabber()
}

@Test void test_detabbing_of_a_small_file() {
def resource = "short-file.txt" // a one-line text file
def filePath = getClass().getClassLoader().getResource(resource).getPath()
def expected = "hello goodbye"
assertEquals "file was detabbed", expected, detabber.detabEntity(filePath)
}
}

class ResourceDetabberTest extends DetabberTest {

@Before void setUp() {
detabber = new ResourceDetabber()
}

@Test void test_detabbing_of_a_small_resource() {
def resource = "short-file.txt"
def expected = "hello goodbye\n"
assertEquals "test resource was detabbed", expected, detabber.detabEntity(resource)
}
}

class FasterResourceDetabberTest extends DetabberTest {
@Before void setUp() {
detabber = new FasterResourceDetabber()
}
}

Results:

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running FasterResourceDetabberTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.409 sec
Running ResourceDetabberTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.11 sec
Running FileDetabberTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.186 sec

Results :

Tests run: 5, Failures: 0, Errors: 0, Skipped: 0

Hopefully, this could bring up some interesting discussion on the pros and cons to this and any other approaches.

3 comments:

  1. I have two issues with the idea.


    1. If all concrete implementations override the detab method, then you will have no tests for the original detab method in the abstract class. This means that method could be broken and you wouldn't know it until the next concrete implementation that didn't override detab came along. That person could inherit a broken method.



    2. Because you are testing 2 classes at once, if something breaks, then you will have to check two different places. This is a problem because that reduces the efficiency of the unit tests. I would technically call this an integration test, as it is testing two different classes at the same time.

    ReplyDelete
  2. I can't disagree. Are you suggesting the approach from the article linked in the JUnit FAQ?
    http://c2.com/cgi/wiki?AbstractTestCases

    ReplyDelete
  3. I think this is interesting. If you are using a unit test to help document how you expect a method to behave then it seems like this is a good way to do it.

    About "anonymous's" issue #2 above, I don't see this as a problem because if you are working with a class that overrides or implements another class then you need to understand both classes anyway...

    ReplyDelete