Skip to content

This project provides a simple example of unit testing in Java and how mocking and dependency injection can both simplify and expand what can be tested in your code.

Notifications You must be signed in to change notification settings

jmctee/Testimonium

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Project Testimonium

Testimonium \ ˌtes-tə-ˈmō-nē-əm- \ (n): Evidence in support of a fact or statement; proof.

This project provides a simple example of unit testing in Java and how mocking and dependency injection can both simplify and expand what can be tested in your code.

The conceipt of the project is that we have a large sensor network (measuring, errr, stuff, yeah, stuff!) we need to collect data from. We need to collect the measurements and summarize them using some standard statistical algorithms, arithmetic mean, median, and standard deviation. In addition, the sensors sometimes cannot collect valid information, so some of the returned readings are not valid. We need to know what percentage of the readings in a collecton are invalid so we know how accurate the statistics on the data are.

Bootstrap Process

To manually bootstrap the project:

Ensure you have java 11 or greater and git installed.

git clone git@github.com:jmctee/Testimonium.git
cd Testimonium

On Macs and Linux:
./gradlew build

On Windows:
.\gradlew.bat build

Project now ready for use, open in the IDE of your choice and got to town!

Project Design

class diagram

A first cut at testing

For this example, we will focus on testing the ColoradoStatsGenerator. We are only interested in verifying the behavior of this class, not others. We call this type of testing "unit testing". But this class depends on other classes. Some of them, like SensorReading and StatsSummary, are POJOs (plain old java objects). We can use them with little concern for any testing issues that they may cause.

The class also depends on a ColoradoSensor, which is a much more complicated class. In this example, it is written to simulate how a real world sensor class might work. IRL, the class would have to make a network connection, post a request to a sensor, wait for the response, then package it up for consumption. We simulate all of this by generating a set of random measurements and add a delay to simulate network and sensor latencies. But we’re pretending all the real world stuff is happening as far as this project is concerned. This class poses a challenge for testing.

As a first step, let’s just assume our tests will use the sensor class as is. This means our test will depend on having a working network connection and sensor to run. And we get back random measurements, with no control of their values, so while an important test to have somewhere (we call these types of tests "functional" or "integration tests", it does not really fit with our stated goal of focusing on ColoradoStatsGenerator. In addition, we can’t control the measurements and behaviors of the sensor class to test any edge cases.

You can see the test I created for this first scenario in the main branch of the project. Look in TestColoradoStatsGenerator to inspect the test. It should be obvious that because the returned sensor readings are random, the test has to do a lot of work to validate the calculations. Would be nice if we could simplify our test code. And as noted earlier, we cannot tune the test to look at edge cases. Finally, the test takes 2 seconds to run! This might not sound like much, but what if we plan to build out a sensor network that numbers in the tens of thousands! It adds up. Slow tests mean slow feedback, which is the opposite of what you want from a test.

Note, for all of issues noted above, this test does pass, so it is better than nothing. But can we do better?

Let’s mock it!

Check out the branch testUsingMocks and we’ll look at a potentially better approach to unit testing.

git checkout testUsingMocks

You’ll see two major changes. First, the constructor of ColoradoStatsGenerator now takes an argument, a Sensor. This technique is often called Dependency Injection (DI). The stats generator depends on a sensor to get its data. Rather than letting the class create its dependency, it is "injected" into the class. From the classes viewpoint, it does not have any knowledge of the Sensor class being injected other than the contract outlined in the interface.

There are several strategies and tools for DI, this one is called constructor injection and is useful for simple cases like this.

Note: Since ColoradoStatsGenerator no longer depends directly on a ColoradoSensor, it should really be renamed, one common pattern when there is only one concrete implementation of an interface is to add Impl the end of the name, e.g., StatsGeneratorImpl. That said, because this is an example and I need to jump back and forth between branches, I am not changing the name.

The second change is to the test. An executeTest method is added where all of the key work takes place. Then tests are written which take advantage of this test method. Each test prepares the data and expected responses, then calls the test method to actually run the test.

Currently, four tests have been implemented:

  • A test of well-formed data where all readings are valid

  • A test of well-formed data where some readings are invalid

  • A test of well-formed data where all readings are invalid

  • A test where no readings are returned

There is obviously room to test many more scenarios, but even with this small set of scenarios, the benefit of mocking and DI should be obvious. By controlling the data returned by the sensor, it is much easier to test scenarios that may be uncommon IRL, but whose behavior, we need to understand/verify.

Also, note the difference in testing time. The test using the physical sensor not only gave us no control of the data, but alos took over 2 seconds to run. The mocked test, which covered four very useful scenarios, took less than 50 milliseconds to run.

Fast and extensive testing!

About

This project provides a simple example of unit testing in Java and how mocking and dependency injection can both simplify and expand what can be tested in your code.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages