Linda Navarette January 13, 2020
It’s no secret to anyone I have worked with that I am an advocate for automated testing. There are a few reasons for my strong opinion of testing:
Forces you to think about the usability and organization of your code – you get to be the first person to use the interface you designed
Provides a language with which to communicate bugs and regressions
Enables you to quickly troubleshoot issues and validate fixes
Serves as supplementary documentation for new team members
In my attempts to convert others into believers, they’ll ask me “where should I go to learn about testing?” To which I always respond, “learn about the test pyramid, then start writing tests.” The only problem is, if you don’t know where to start, the number of resources on testing can be overwhelming. I’ve compiled some suggestions based on what has worked for me.
The Test Pyramid
Before writing any tests, let’s start with some theory and fundamentals in order to:
Gain a high-level familiarity with the types of tests you’ll often see in practice
Give a pragmatic approach to where to spend your time and energy as you learn
This practical test pyramid article explains the test pyramid and gives a brief introduction to some types of tests you’ll encounter in practice. To highlight a few key points made:
The original test pyramid had only three levels (unit, service, UI). In modern architectures, many teams are building software platforms, not a single app. With more interaction points came new levels of the pyramid, such as integration tests and contract tests.
You can – and many people do – refer to each level by a variety of names. For example: component tests, acceptance tests, and functional tests are sometimes used interchangeably and sometimes used to differentiate between whether you’re testing the functionality of a single service/component versus the entire system.
Keep the balance illustrated in the pyramid: most of your tests should be fast and easy to diagnose unit tests, while very few should be long-running and brittle end to end tests.
If you’re genuinely new to testing, you should focus on unit testing first. Unit tests are the fastest, easiest to implement and diagnose, and make up the bulk of your tests.
Tooling and Methodologies
When writing tests, the framework you use, to a large extent, influences the style and methodologies you use to implement your test. Beyond the framework, you can adopt your own standards with naming conventions, code style, and additional libraries for mocking and assertions. Consistency with regard to writing tests helps you to be able to maintain and diagnose test failures as well as onboard new team members to do the same.
A test framework provides the runner (which executes tests) and the guidelines/conventions for writing tests. A family of unit test frameworks has emerged over time with similar conventions and functionality, they’re generally referred to as “xUnit” but specific implementations replace the x with the first letter of the language. In the Java community, where I have the most experience and will focus most of my advice, there’s JUnit. Other implementations include xUnit.net and NUnit for .NET, unittest/pyUnit for Python, and many more. In most frameworks, tests are broken down into test suites, test classes, and test methods. For each class you’ve implemented, you should have a corresponding test class for unit tests. Each test method then invokes one specific code path within a method in your source. This introduction to the JUnit 5 Test Annotation explains how to implement a test method. To avoid duplicate code within your tests, setup and teardown methods are supported by many frameworks to provide logic common across all test methods within a class or suite. After you’ve written a few unit tests using just those basic constructs, take a deeper look at more JUnit 5 tutorials. As you get more comfortable, you can leverage other features to simplify the act of implementing tests and diagnosing failures.
You may have noticed in that test annotation tutorial that all of the example tests were named similarly:
As mentioned in the test pyramid article, this convention comes from the Behavior Driven Development (BDD) style of testing. This methodology states that your design (and therefore your tests) should be defined in plain text, usually to the extent that a non-technical person could read the name of the test and understand what behavior you’re trying to validate. I personally am a proponent of BDD testing for exactly that reason: I want to be able to look at a test and quickly know its exact purpose. Another slight variation that I tend to use looks like:
For unit testing, where the focus is on the unit under test (ie: the method), this naming convention highlights that information.
When writing unit tests, your goal is to validate the responsibilities of the class and method under test, which often means you’ll often not want to use the object’s real dependencies. The implementation you’d inject was conceptually referred to as a test double in the test pyramid article, but colloquially you’ll often hear this practice referred to as “mocking”.
Mockito [official, tutorial] is the commonly used mocking framework in the Java community
Powermock [official, tutorial] provides additional features for testing code that is generally considered untestable, allowing you to mock static or private methods. It is worth noting that needing to do this is generally indicative of a design issue.
Along with mocking the desired outcomes, you can also verify the desired interactions occurred.
Assertions are the part of your test that actually do validation – without an assertion, a test doesn’t provide any value. Frameworks tend to come with built-in assertions, but there are also many independent assertions libraries that exist. An independent assertion library can be used alongside or in place of JUnit assertions – usually for improved messages on failure or for additional features. Some I’ve used include:
AssertJ [official, tutorial] is a fluent API for assertions; if you’re not familiar with fluent APIs, that just means it’s designed to read like plain text. You’ll often see fluent assertions along with BDD-style tests because the reasoning behind those methodologies is very similar.
Hamcrest [official, tutorial] provides matchers to support assertions more nuanced than the traditional “is equal to” assertion.
How many assertions should I have in each test?
This question, like many questions related to testing, largely comes down to philosophy. The reason many people advocate for exactly one assertion in every test is because it’s a good hygienic practice that ensures that a unit test will only ever fail for one reason. In most instances I would agree, however, there are instances where including a couple of related assertions in the same test allows you to give a better message when the test fails.
Start Writing Tests
A lot of testing ultimately comes down to intuiting how you’ll derive the most value from the test failing in the future. You’ll only gain this skill from writing tests, troubleshooting failed tests, and most importantly: fixing the bad tests you wrote as you were learning. Let me know where you struggle writing tests, what topics I can cover in more depth, or what types of tests you want to learn more about.