Chapter 7. Testing with Gradle
This chapter covers
- Understanding automated testing
- Writing and executing tests with different frameworks
- Configuring and optimizing test execution behavior
- Supporting unit, integration, and functional tests in your build
In the previous chapters, you implemented a simple but fully functional web application and learned how to build and run it using Gradle. Testing your code is an important activity of the software development lifecycle. It ensures the quality of your software by checking that it works as expected. In this chapter, we’ll focus on Gradle’s support for organizing, configuring, and executing test code. In particular, you’ll write unit, integration, and functional tests for your To Do application and integrate them into your build.
Gradle integrates with a wide range of Java and Groovy unit testing frameworks. By the end of this chapter, you’ll write tests with JUnit, TestNG, and Spock, and execute them as part of the build lifecycle. You’ll also tweak the default test execution behavior. You’ll learn how easy it is to control the test logging output and to add a hook or listener to react to test lifecycle events. We’ll also explore how to improve the performance of big test suites through forked test processes. Integration and functional tests require a more complex tooling setup. You’ll learn how to use the third-party tools H2 and Geb to bootstrap your test code.
We’re not going to cover the details of why an automated testing approach is beneficial to the quality of your project. There are many excellent books that cover this topic. Long story short: if you want to build reliable, high-quality software, automated testing is a crucial part of your development toolbox. Additionally, it’ll help reduce the cost of manual testing, improve your development team’s ability to refactor existing code, and help you to identify defects early in the development lifecycle.
Traditionally, test code in Java is written in Java. Popular open source testing frameworks like JUnit and TestNG help you write repeatable and structured tests. To execute these tests, you’ll need to compile them first, as you do with your production source code. The purpose of test code is solely to exercise its test cases. Because you don’t want to ship the compile test classes to production systems, mingling production source and test code isn’t a good idea. Optimally, you’ll have a dedicated directory in your project that holds test source code and another that acts as a destination directory for compiled test classes.
Gradle’s Java plugin does all of this heavy lifting for you. It introduces a standard directory structure for test source code and required resource files, integrates test code compilation and its execution into the build’s lifecycle, and plays well with almost all of the popular testing frameworks. This is a significant improvement over implementing the same functionality in an imperative build tool like Ant. You’d easily have to write 10 to 20 lines of code to set up a testing framework for your code. If that wasn’t enough, you’d have to copy the same code for every project that wants to use it.
As a Java developer, you can pick from a wide range of testing frameworks. In this section, you’ll use the traditional tools JUnit and TestNG, but also look at the new kid on the block, Spock. If you’re new to any of these testing frameworks, refer to their online documentation, because we won’t cover the basics of how to write a test.
Test execution is an essential and important phase in the lifecycle of your build. Gradle gives you a wide variety of configuration options in your build script, as well as command-line parameters to control the runtime behavior. How and when you apply these options depends on what you need in your build. This section will give you a short and sweet overview of frequently used functionality and the API classes behind these options. Let’s start with some helpful command-line options.
A unit test verifies that the smallest unit of code in your system, a method, works correctly in isolation. This allows you to achieve a fast-running, repeatable, and consistent test case. Integration tests go beyond the scope of unit tests. They usually integrate other components of your system or external infrastructure like the file system, a mail server, or a database. As a result, integration tests usually take longer to execute. Oftentimes they also depend on the correct state of a system—for example, an existing file with specific content—making them harder to maintain.
Functional testing is ideal for verifying that the software meets the requirements from the end user’s perspective. In the context of your web application, this means simulating the user’s interactions with the browser, such as entering values into text fields or clicking links. Historically, functional tests have been hard to write and costly to maintain. You need a tool that automates bringing up the browser, manipulates the data object model (DOM) of the web page, and supports running these tests against different browsers. On top of that, you also need to integrate the functional tests into your build to be able to run them in an automated and repeatable fashion. Let’s look at a specific use case and an automation tool that can help you test-drive the tests.
In this chapter, you learned how to implement unit tests with the help of three popular testing frameworks: JUnit, TestNG, and Spock. Gradle’s Test API plays a significant role in configuring the test execution to your needs. The two examples we discussed in great detail can be directly applied to a real-world project. Being able to have fine-grained control over your test logging is a huge benefit when trying to identify the root cause of a failed test. Test classes that are part of large test suites can be run in parallel to minimize their execution time and utilize your hardware’s processing power to its full capacity.
Integration and functional tests are harder to write and maintain than unit tests. Integration tests usually involve calling other components, subsystems, or external services. We discussed how to test an application’s data persistence layer in combination with a running SQL database. Functional tests verify the correctness of your application from the user’s perspective. With the help of a test automation framework, you remote-controlled the browser and emulated user interaction. You configured your build to provide a source set for different types of tests, provided new test tasks, fully integrated them into the build lifecycle, and even bootstrapped the test environment where needed.