Understanding the nature of a unit test
A unit test is basically a piece of code written by a developer to verify that another piece code—usually the implementation of a feature—works correctly. In this context, a unit identifies a very small, specific area of behavior and not the implementing code itself. If we regard adding an item to our timeline as a functional feature for example, appropriate tests would ensure that the item list grows by one and that the new item gets inserted at the right chronological position.
Yet, there is more to it than meets the eye. Unit tests are restricted to that code for which the developer is responsible. Consider using a third-party library that relies on external resources. Tests would implicitly run against that third-party code. In case one of the external resources is not available, a test could fail although there might be nothing wrong with the developer's code. Furthermore, set up could get painstaking, and due to the invocation time of external resources, execution would get slow.
But we want our unit tests to be very fast because we intend to run them all as often as possible without impeding the pace of development. By doing so, we receive immediate feedback about busting a low-level functionality. This puts us in the position to detect and correct a problem as it evolves and avoid expensive quality assurance cycles.
As the book progresses, we will see how to deal with the integration of third-party code properly. The usual strategy is to create an abstraction of the problematic component. This way, it can be replaced by a stand-in that is under the control of the developer. Nevertheless, it is important to verify that the real implementation works as expected. Tests that cope with this task are called integration tests. Integration tests check the functionality on a more coarse-grained level and focus on the correct transition of component boundaries.
Having said all this, it is clear that testing a software system from the client's point of view to verify formal specifications does not belong to unit testing either. Such tests simulate user behavior and verify the system as a whole. They usually require a significant amount of time for execution. These kinds of tests are called acceptance or end-to-end tests.
Another way to look at unit tests is as an accompanying specification of the code under test, comparable to the dispatch note of a cogwheel, which tells Quality Assurance (QA) what key figures this piece of work should meet. But due to the nature of the software, no one but the developer is apt to write such low-level specifications. Thus, automated tests become an important source of information about the intended behavior of a unit and one that does not become outdated as easily as documentation.
Note
We'll elaborate on this thought in Chapter 2, Writing Well-structured Tests.
Now that we've heard so much about the nature of unit tests, it's about time to write the first one by ourselves!
| "A journey of a thousand miles begins with a single step." | |
| --Lao Tzu |
Unit tests written with JUnit are grouped by plain Java classes, each of which is called a test case. A single test case specifies the behavior of a low-level component normally represented by a class. Following the metaphor of the accompanying specification, we can begin the development of our timeline example as follows:
The test class expresses the intent to develop the a component Timeline
, which Meszaros, [MESZ07], would denote as system under test (SUT). And applying a common naming pattern, the component's name is complemented by the suffix Test
. But what is the next logical step? What should be tested first? And how do we create an executable test anyway?
Usually, it is a good idea to start with the happy path, which is the normal path of execution and, ideally, the general business use case. Consider that we expect fetch-count to be an attribute of our timeline component. The value configures how many items will be fetched at once from an item source. To keep the first example simple, we will ignore the actual item loading for now and regard only the component's state change that is involved.
An executable JUnit test is a public, nonstatic method that gets annotated with @Test
and takes no parameters. Summarizing all this information, the next step could be a method stub that names a functionality of our component we want to test. In our case, this functionality could be the ability to set the fetch-count to a certain amount:
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
Additionally, the author has hosted the code sources for this book on his GitHub repository at https://github.com/fappel/Testing-with-JUnit. So, you can download it from this URL and work with the code.
This is still not much, but it is actually sufficient to run the test for the first time. JUnit test executions can be launched from the command line or a particular UI. But for the scope of this book, let's assume we have IDE integration available. Within Eclipse, the result would look like the next image.
The green progress bar signals that the test run did not recognize any problems, which is not a big surprise as we have not verified anything yet. But remember that we have already done some useful considerations that help us to populate our first test easily:
- We intend to write the
Timeline
component. To test it, we can create a local variable that takes a new instance of this component. - As the first test should verify the state-changing effect of setting the item-count attribute, it seems natural to introduce appropriate setters and getters to do so:
It looks reasonable so far, but how can we assure that a test run is denoted as a failure if the actual value returned by getFetchCount
does not match the input used with setFetchCount
? For this purpose, JUnit offers the org.junit.Assert
class, which provides a set of static methods to help developers to write so-called self-checking tests.
The methods prefixed with assert are meant to check a certain condition, throwing an java.lang.AssertionError
on a negative evaluation. Such errors are picked up by the tool's runtime and mark the test as failed in the resulting report. To assert that two values or objects are equal, we can use Assert.assertEquals
. As it is very common to use static imports for assertion method calls, the getFetchCount
test can be completed like this:
The built-in mechanism of JUnit, which is often considered somewhat dated, isn't the only possibility to express test verifications. But to avoid information flooding, we will stick to it for now and postpone a thorough discussion of the pros and cons of alternatives to Chapter 7, Improving Readability with Custom Assertions.
Looking at our first test, you can recognize that it specifies a behavior of the SUT, which does not even exist yet. And by the way, this also means that the test class does not compile anymore. So, the next step is to create a skeleton of our component to solve this problem:
Well, the excitement gets nearly unbearable. What will happen if we run our test against the newly created component?
Now the test run leads to a failure with a red progress bar due to the insufficient implementation of the timeline component, as shown in the next image. The execution report shows how many tests were run in total, how many of those terminated with errors, and how many failed due to unmet assertions.
A stack trace for each error/failure helps to identify and understand the problem's cause. AssertionError
raised by a verification call of a test provides an explaining message, which is shown in the first line of the trace. In our example, this message tells us that the expected value did not meet the actual value returned by getFetchCount
.
A test terminated by an Exception
indicates an arbitrary programming mistake beyond the test's assertion statements. A simple example of this can be access to an uninitialized variable, which subsequently terminates test execution with NullPointerException
. JUnit follows the all or nothing principle. This means that if an execution involves more then one test, which is usually the case, a single problem marks the whole suite as failed.
The UI reflects this by painting the progress bar red. You would now wonder whether we shouldn't have completed our component's functionality first. The implementation seems easy enough, and at least, we wouldn't have ended up with the red bar. But the next section explains why starting with a failing test is crucial for a clean test-first approach.
Writing tests before the production code even exists might look strange to a newbie, but there are actually good reasons to do so. First of all, writing tests after the fact (meaning first code, then test) is no fun at all. Well, that sounds like a hell of a reason because, if you gotta do what you gotta do, [FUTU99], what's the difference whether you do it first or last?
The difference is in the motivation to do it right! Once you are done with the fun part, it is all too human to get rid of the annoying duties as fast and as sloppily as one can get through. You are probably reading this because you are interested in improving things. So, ask yourself how effective tests will be if they are written just for justification or to silence the conscience.
Even if you are disciplined and motivated to do your after the fact tests right, there will be more holes in the test coverage compared to the test-first approach. This is because the class under test was not designed for testing. Most of the time, it will take costly steps to decompose a component written from scratch into separate concerns that can be tested easily. And if these steps are considered too expensive, testing will be omitted. But isn't it a bad thing to change a design for testing purposes?
| "Separation of Concerns' is probably the single most important concept in software design and implementation." | |
| --[HUTH03] |
The point is that writing your tests first supports proper separation implicitly. Every time your test setup feels overly complicated, you are about to put too much functionality in your component. In this case, you should reconsider your class-level design and split it up into smaller pieces. Following this practice consequently leads to a healthy design on the class level out of the box.
Although this book is not about how to write tests first or test-driven development (TDD) as it is usually called, it follows this principle while developing the example application. But as the focus will be on getting unit tests right and not on the implementation aspects of the components, here come a few words about the work paradigm of TDD for better understanding.
The procedure is simple. Once you have picked your first work unit, write a test, make it run, and last, make it right, [BECK03]. After you're done, start it all over again with the next piece of functionality. This is exactly what we have done until now with our first test. We've decided about a small feature to implement. So, we wrote a test that specifies the intended behavior and invented a kind of programming interface that would match the use case.
When we feel confident with the outcome, it is about time to fix the compile errors and create a basic implementation stub to be able to execute the test. This way, the test is the first client of the freshly created component, and we will have the earliest possible feedback on how using it in programs will look. However, it is important that the first test run fails to ensure that the verification conditions were not met by accident.
The make it run step is about fixing the failing test as quickly as possible. This goal outweighs everything else now. We are even allowed to commit programming sins we usually try to avoid. If this feels a bit outlandish, think of it like this: if you want to write clean code that works (Ron Jeffries, [BECK03]) ensure that it works first and then take your time and clean it up second. This has the advantage that you know the specification can be met without wasting time in writing pretty code that will never work.
Last but not least, make it right. Once your component behaves as specified, ascertain that your production and test code follow the best programming standards you can think of. While overhauling your code, repeatedly executing the tests ensures that the behavior is kept intact. Changing code without changing its behavior is called refactoring.
In the overall image, we started with a failing test and a red bar, fixed the test, made the bar green again, and, finally, cleaned up the implementation during a last refactor step. As this pattern gets repeated over and over again in TDD, it is known as the red/green/refactor mantra.
So, always remember folks: keep the bar green to keep the code clean.