A bug in software development is common, but some times it can cost a lot. The most known case of a bug that cost million of dollars was the Mariner 1 spacecraft. An investigation revealed the cause. A very simple software error, the developer omitted an hyphen in a line of code. That sent an incorrect guidance signals to the spacecraft. The cost of the omission turned out to be more than $18 million at the time (about $169 million in today’s currency).
So, don't lose money discovering bugs in production. Prevent it with a simple method, use tests in the development stage. Here you will learn one type of test for Java projects, and how to create and run them in during development. At the end, you will be ready to configure your project with tests. Use libraries, plan the scenarios and create the tests and mocks. Finally, you will prevent bugs in production.
Prevent bugs with Unit Test
The most simple test is unit test. It need to be simple and test only one
scenario in isolated way.
Mock
In a unit test, the main goal is to test some behavior from one unit (a class in Java). A mock is a way to create a object that the unit that you are testing depends on. So, If you are testing a Class called ExportToJsonService
and this class depends on a another class called FileService
, you can test the ExportToJsonService
without instantiate the real FileService
implementation, you can use some library to create a Mock of FileService
. The mock will be a object of FileService
, but without any implementation of the behaviors from the real implementation.
Create a unit test
Now, you know about unit test and mocks. Before starting creating a tests, we need libraries to don't reinvent the wheel. There are two Java libraries that can help you to create unit tests in JAVA:
- Junit
- EasyMock
The first one is a platform that help a java developer to create tests and run in a JVM. The second is a library that help you to isolate the test creating mocks for you. In the next sessions, you will learn how to use theses libraries in development proccess. In the end, you will find the full example of a project with theses libraries in a github repository.
Project example
So, let define some project to exemplify the use of the java test libraries. This project is simple, it generates a PDF certificate to participants of an event. So, there are two features in this project:
- Generate a certificate in PDF format
- Generate a certificate metada in JSON format
So, we designed the project to have these classes:
GenerateCertificate
public class GenerateCertificate {
private final ExportService pdfExportService;
private final ExportService jsonExportService;
public GenerateCertificate(ExportService pdfExportService, ExportService jsonExportService) {
this.pdfExportService = pdfExportService;
this.jsonExportService = jsonExportService;
}
public void generateCertificate(CertificateData certificateData) {
// Generate Object Certificate from data
// Export a PDF file from certificate object
// Export a Json metadata from certificate object
}
}
ExportService
public interface ExportService {
void export(Certificate certificate);
}
Planning your tests
The creation of tests start from the scenarios that you need to test. There are two types of scenarios:
- Success when the input are correct
- Error when the input are incorrect
So, the first test of GenerateCertificate is when the CertificateData is enough to generate a Certificate objetct that
can be exported in PDF and in JSON. Moreover, the second test is when there is something in the CertificateData that
lead to a error when try to create a certificate object.
Therefore, the first test is when the certificate object passed to exportService instance are correct and the two
instances of the exportService is called.
First unit test
After plan the test, develop it with Junit and EasyMock. We can create a Test with Junit 5
with a Class that
had @Test
above a method. So, the method annotated is the first scenario.
GenerateCertificateTest
import org.junit.jupiter.api.Test;
public class GenerateCertificateTest {
@Test
public void whenGenerateCertificate_withCertificateDataValid_thenExportCertificate() {
}
}
We created a test for the unit. Now we need to create a instance of our unit: GenerateCertificate
. After, we need to call the
method to test our scenario inside the method of the test with the correct inputs:
import dev.alexferreira.testtechniques.example.certificate.model.Certificate;
import dev.alexferreira.testtechniques.example.certificate.model.CertificateData;
import dev.alexferreira.testtechniques.example.certificate.service.ExportService;
import org.easymock.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
class GenerateCertificateTest {
private final GenerateCertificate generateCertificate;
@Test
void whenGenerateCertificate_withCertificateDataValid_thenExportCertificate() {
CertificateData certificateData = new CertificateData();
generateCertificate.generateCertificate(certificateData);
}
}
After, We use the EasyMock library to create mocks and call's verification. So, we can create a scenario to test some code flux. See the example above:
import dev.alexferreira.testtechniques.example.certificate.model.Certificate;
import dev.alexferreira.testtechniques.example.certificate.model.CertificateData;
import dev.alexferreira.testtechniques.example.certificate.service.ExportService;
import org.easymock.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(EasyMockExtension.class)
class GenerateCertificateTest extends EasyMockSupport {
/*01**/
@Mock
/*02**/ private ExportService exportService;
/*03**/
/*04**/
@TestSubject
/*05**/ private final GenerateCertificate generateCertificate = new GenerateCertificate(null);
/*06**/
/*07**/
@Test
/*08**/ void whenGenerateCertificate_withCertificateDataValid_thenExportCertificate() {
/*09**/
exportService.exportPDFFile(EasyMock.anyObject(Certificate.class));
/*10**/
replayAll();
/*11**/
/*12**/
CertificateData certificateData = new CertificateData();
/*13**/
generateCertificate.generateCertificate(certificateData);
/*14**/
/*15**/
verifyAll();
}
}
Test detailed
Let's analyse the test:
- line 1 - 2: define a mock exportService. The annotation specific to create an instance without any behavior.
- line 4: create a instance of the unit and inject the mocks.
- line 5: define the object to test and inject the mocks
- line 7-16: define the block to test and Junit execute it
- line 10: call
replayAll
to specify for EasyMock the mocks to verify after that point. - line 15: call
verifyAll
to configure EasyMock to verify all calls in all mocks created. It will throw an error when some call are not expected.
The test will verify the calls to generateCertificate
. If exportPDFFile()
was not called, the test will fail.
Therefore, the EasyMock will help you to validate the unit dependencies behaviors. You can see more features from EasyMock at the official documentation.
Running a test
So, at this point, we can run the test with maven: mvn test
or with gradle: gradlew test
. The test will fail, because we did not implement the function like the test expect. According to Test driven development (TDD), we should implement the unit in a cyclic process. You need run test, implement/fix and run test. So, the implementation is incomplete until the test had passed. Find more information about TDD here.
Versions used in this example
The example used in theses tutorial is hosted
by Github. The versions used are following:
- Junit: 5
- EasyMock: 4.3
- Java 11
Conclusion
We presented how to prevents bugs with unit tests, the libraries that you can use to create the tests and mocks. Now you can prevent bug in production.
We used two libraries that are enough for a developer create unit tests to his software. But, you need to pay attention to the versions and the compatibility between them. There are another libraries, but this article is not comparing them. We is showing one example of library for each requirements to create tests.
I am a Java Developer that help another Java developers to apply tests techniques in software development, so they deliver stable features faster without break things.
Top comments (0)