1 Introduction

In this lab you will be given the opportunity to test a simple application, reflect on its testability, and then try to improve it.

You are given a sequence of problems to solve:

1.1 Preparation

In this lab you will be working with the lab6-testability project. It contains a set of Java classes under src/main/java, and a very simple TestNG test class under src/test/java. You can open the project and run the test in your favorite IDE, or you can build and test it in your shell using Maven: mvn test. Note that you should be able to compile the code, but the test is intended to fail!

For the IDA labs and thinlinc, run the following command to make mvn work correctly (if you use the command line instead of Eclipse):

JAVA_HOME=`readlink -f /usr/bin/java`; JAVA_HOME=`dirname $JAVA_HOME`; JAVA_HOME=`dirname $JAVA_HOME`

If you don’t have Maven set up, download and install it. If you use Maven from the command-line, you can refer to its documentation

Tip

In Eclipse, use Import -> Maven -> Existing Maven Projects. Running Maven Test will then download TestNG and other dependencies used in the project.

1.2 Note on Mocking

If you are not familiar with mocking, you can skip this section and move on to the exercises.

If you are familiar with mocking, perhaps you will take issue with some of the exercises: where improving the testability of the item under test is suggested by the instructions, the problem could instead be solved by using one of several available mocking frameworks. This is true, but consider the following:

  • Mocking frameworks are highly useful, not least in that they can help overcome poor testability. This lab, on the other hand, is about exploring and improving testability, rather than techniques for testing despite lack of testability.
  • Testability is tightly related to flexibility: testing software can be thought of as using it in a context and for a purpose other than what it was originally designed for. Therefore, highly testable code tends to also be highly flexible for other purposes. In other words, testability has positive knock-on effects.
  • The techniques for improving testability explored in this lab will also make mocking easier. By giving control over the collaborators to the test, you also provide the ability to replace these collaborators with stubs, mocks or variants.

2 Exercises

The lab exercises are designed to be solved in sequence.

Note

You are writing code for testing the class. As such, you probably do not want to add new methods to classes (in particular not public methods) in too many places. If you feel like an elegant solution would be to add a method, consider what the consequences would be.

Tip

If you git commit after each completed step, you can see what you did in each step in order to write a better lab report.

2.1 Asking for Dependencies

Begin by exploring the se.liu.ida.tdde45.Resident class. It has methods for making breakfast and lunch, and it has one collaborator to help with this: a private instance of se.liu.ida.tdde45.House. Now take a look at se.liu.ida.tdde45.ResidentTest. This has a single method to test the breakfast-making. Note that it doesn’t verify any breakfast: as long as no exceptions are raised, this test will pass. Run it and see what happens!

Clearly there’s a problem. It would seem that the fridge needs to be stocked. Note that there’s a se.liu.ida.tdde45.singletons.FridgeStocker with a stock(Fridge fridge). This should be able to help, but there’s another catch: how do we get a hold of the fridge to stock? Resident uses the fridge indirectly, but it doesn’t expose its collaborators.

Think about how Resident might be updated to allow the test to stock the fridge. When you consider solutions, think about whether the class has any reason to expose its collaborators other than to support the test. In other words, rather than exposing its House to the outside world (or adding a getter for the Fridge), is there anything else that can be done to improve the testability?

Tip

The name of the exercise is a hint!

Tip

The constructor is a good place to ask for the things you need.

Note

You do not need the test to pass to finish this exercise.

Tip

You do not want to stock the fridge in its constructor as you may want to test an empty fridge.

2.2 Object Construction Versus Collaboration Graphs

One way to help us reason about this application is to separate object construction dependencies from collaborator dependencies. Both of these form graphs. There are tools that can help us render such graphs, but this example is simple enough that we can do it with pen and paper.

Start from your test method and sketch an object construction graph: which classes construct which other classes? To help you get started, ResidentTest creates a House and a Resident. Which objects to these classes create in turn?

Once you have exhausted the tree and thereby mapped out the construction of the application, repeat the exercise, but this time map collaborations. Which classes collaborate with which other classes? To help you get started, ResidentTest invokes Resident.makeBreakfast(). Does it invoke methods in other classes? Who does Resident collaborate with? Don’t include simple get() methods reaching through objects to get at the actual collaborator.

2.3 Hidden Dependencies

In the first exercise, we found that Resident has a dependency for making breakfast: unless FridgeStocker stocks the Fridge, breakfast can’t be made. Looking at the Resident class from the outside, examining its constructor and method signatures, were there any clues that there were any such dependencies to Fridge, much less FridgeStocker? We can think of this as a hidden dependency: Resident will happily tell you that it doesn’t need anything, it can make breakfast out of thin air, but in reality it needs collaborators. Rather than telling you about them, it will contact these collaborators in secret.

Now that we found this out and fixed the problem in the first exercise, we should be able to make some breakfast. If you haven’t re-run the test yet to check your solution, do that now.

Now we get a new exception. Apparently FridgeStocker needs to be initialized. Explore the code and figure out how you can initialize it to make the test pass. Also consider the implications of this: did anything about Fridge tell you that it not only needs a FridgeStocker, but an initialized one?

Now re-run the test and keep initializing singletons until you have cleared all the UninitializedSingletonException.

Once everything down to BankConnector has been initialized it would appear we got to the bottom of the problem, but now something else went wrong. Since we have been using real classes for real transactions, our testing of Resident appears to have had some unforeseen side-effects! Think about how you solved the first exercise and refactor ChargingQueue to let you provide it with a harmless version of a BankConnector object, rather than having it fetch a singleton.

Having sorted out the hidden singleton dependencies, you should come further in your attempts to test Resident.makeBreakfast(). Now it appears that the stove needs to be turned on in order to boil eggs. Since we already have control over the House object we can get the Stove from there. Let the test case turn on the stove, run it and see that it passes.

2.4 Global State

The testMakeBreakfast() method is actually rather poorly named. A good practice is to name test methods such that it’s clear which behavior they’re testing. Now we’ll test that the application is well behaved even when it doesn’t work. Create a new test method below the first one called testMakeBreakfastFailsIfFridgeStockerIsNotInitialized(). Now we want to verify that we actually get the UninitializedSingletonException as expected. This should be very similar to the first test method: initialize all the singletons except FridgeStocker. This time we expect an exception. There are different ways you can do this; TestNG lets you conveniently express expected exceptions using the @Test annotation:

@Test(expectedExceptions = UninitializedSingletonException.class)

Now run the test. If you’ve followed the instructions, the new test will fail. Why? Here’s a clue: try commenting out the testMakeBreakfast() test case and run only the new test method. (If your new test case doesn’t fail, make sure you actually executed both tests, and that TestNG executed testMakeBreakfast() first.)

The problem here is that these singletons represent global state, and anything that interacts with them becomes interdependent through them. In this case, this has created a dependency between the test cases: we can only test what happens when using an uninitialized singleton if another test case hasn’t already initialized it, unless we want to restart the JVM in between test cases. This is bad: you always want your test cases to be independent so you can execute them in any order, any number of times, or only some of them.

The solution here is similar to the previous exercises: turn FridgeStocker into a regular object that you can inject into the item under test. Which object should get the FridgeStocker collaborator is an interesting question: is the Resident responsible for stocking the Fridge before fetching ingredients, or is the Fridge intelligent enough to call the FridgeStocker? To make an informed decision on this we would need to know more about the purpose of the application - for the sake of the exercise, let’s decide to inject FridgeStocker into Fridge. To allow the test to control the Fridge you have to refactor several classes: the Fridge must be injected into the Kitchen and the Kitchen into the House.

Once this has been refactored each individual test case has control over whether and when the FridgeStocker is initialized. You should now have no trouble making testMakeBreakfastFailsIfFridgeStockerIsNotInitialized() work.

Note

You may want to rename UninitializedSingletonException if the class is no longer a Singleton. You are allowed to keep the class name even if there is no Singleton functionality remaining in the program.

2.5 Law of Demeter

We’re making progress! We now have two working test cases, one verifying the happy path through Resident.makeBreakfast() and one a sad path. Both test methods have quite lengthy setup procedures, with big chunks of boilerplate code. Why do we need to create all these objects? Does Resident need the House and the Kitchen to prepare its meals? And what does this do for the maintainability of the code? What will happen to our breakfast-making tests if we put in a bedstand in the bedroom, or add a new room to the house?

The current setup is a clear violation of the Law of Demeter (read up on it if you are unfamiliar with it). Resident reaches through House and Kitchen to get to its actual collaborators. In this simplified example this is only a minor inconvenience, but let’s assume that both House and Kitchen in a real world scenario are heavy-weight and non-trivial to construct: a house might contain any number of rooms, with any amount of furniture and appliances. We already refactored Resident to ask for its House rather than build it; now refactor it further to ask for its actual collaborators. We still end up with some boilerplate, but much less, and more importantly we don’t run the risk of side-effects in parts of the code we’re not really interested in, and isolate ourselves from changes in irrelevant objects.

Tip: If you want to, you can extract the object creation boilerplate into a separate method annotated @BeforeMethod. This will be executed prior to each test method execution, greatly helping you clean up your code, but that’s not the focus of this lab.

2.6 Object Substitution

An important benefit of working with objects, rather than with global state singletons such as OrderDispatcher and CreditCardCharger is that they can easily be substituted, thereby controlling the behavior of the item under test (or for other purposes - remember, everything we do here has ramifications for flexibility in general). For the purposes of these tests, whether making breakfast ends up changing the balance of some bank account is irrelevant: we’re only interested in Resident and its nearest collaborators.

Since we already refactored FridgeStocker into an injectable object, we can actually cut off its hidden dependency on OrderDispatcher and everything else. In your test code, create a class that extends FridgeStocker and overrides its stock() method, so that it no longer calls OrderDispatcher. While you’re at it, remove the requirement to initialize it as well - the only thing we’re interested in is that it stocks the fridge!

Now we can test Resident without any round-trips through the entire application: we can focus on ensuring its correct internal behavior and correct interactions with Fridge, Stove and Workbench. Remove all the singleton initializations as well as the BankConnector stub in your test code: they’re not needed anymore.

Depending on how you implemented your FridgeStocker stub your testMakeBreakfastFailsIfFridgeStockerIsNotInitialized() method might fail. If you don’t throw an exception from it anymore, you won’t be able to verify that you get exceptions from it. If you preserved the exception throwing in the stub, ask yourself what the purpose of the test method is now: why are you running a test that verifies the functionality of a stub that is also nothing but test code? Through our refactoring of the code it has become much clearer that what might have seemed like a perfectly valid thing to test in the first place actually had nothing to do with the Resident class to begin with, and therefore shouldn’t be part of any ResidentTest method. In other words, we can safely remove this test method as well as the BankConnector stub.

Tip: Creating these substitutions by hand is rather awkward. This is where mocking frameworks shine: they make this type of substitution extremely easy, and then let you interrogate the mock objects to determine whether your object under test has behaved as expected.

In case you extracted the object creation part into a separate @BeforeMethod method, your testMakeBreakfast() method should now look something like this:

@Test
public void testMakeBreakfast() throws UnknownIngredientException,
UnpoweredException, UninitializedSingletonException {
  stove.turnOn();
  r.makeBreakfast();
}

2.7 A Few Quick Ones

Now that the code is more amenable to testing, let’s see if adding new tests has become any easier. Create two new test methods: one verifying that cooking on a filthy stove will make the Resident sick, and one verifying that the Resident doesn’t get sick even if the stove is filthy, if the stove isn’t turned on (preventing the meal from being prepared in the first place).

What was your experience writing these tests? Would writing them have been easier or harder before all the refactoring of the code?

2.8 Graphs Revisited

We have now refactored the application quite a bit. Let us repeat the exercise from Section 2.2. Which differences do you spot? Which conclusions can you draw from this?

Clue: To effectively test the application logic, you need to do that in well defined isolated parts of the application. This requires control over object construction. How might this be expressed in terms of overlap versus separation of construction and collaboration graphs?

3 Final Note

There are many more things that can be improved in this example application, and the end result of all the exercises is far from perfect (for instance, we didn’t clean up the fact that non-singletons are now throwing UninitializedSingletonException). While this is the end of the required lab exercises, feel free to keep experimenting with ways to further increase the testability and flexibility of the code by controlling object creation and dependency graphs in the application. Try writing tests for other parts of the software, such as Resident.makeLunch(). Remember to not only test happy paths, but also sad paths!