I've shared how to quickly get started with open source projects and how to do integration testing in open source projects, but I haven't talked about specific hands-on exercises.
Today to talk in detail about how to write unit tests.
🤔 When do you need unit tests?
This should be a consensus, for some single function, the core logic, while changes are infrequent public function is necessary to do unit testing.
It is often recommended to do e2e testing for functions that are complex in terms of business, cumbersome in terms of links, but also core processes, so as to ensure the consistency of the final test results.
💀 Specific cases
We all know that the main purpose of a single test is to simulate the execution of every line of code you've ever written, with the goal of covering the major branches and doing so with every line of your own code in mind.
summarized belowApache HertzBeat
of some single tests as an example of how to write a unit test.
Let's start with one of the simplest#preCheck
function test as an example.
Here.preCheck
The function is simply a test to do parameter checksums.
The test is done as long as we manually set themetrics
set tonull
You can enter this if condition.
@ExtendWith()
class UdpCollectImplTest {
@InjectMocks
private UdpCollectImpl udpCollect;
@Test
void testPreCheck() {
List<String> aliasField = new ArrayList<>();
("responseTime");
Metrics metrics = new Metrics();
(aliasField);
assertThrows(, () -> (metrics));
}
}
Coming to the specific single test code, let's look at it line by line:
@ExtendWith()
beJunit5
An annotation provided in which the passed-in This is a common framework we use for single-test mocking.
Simply put, it tellsJunit5
The current test class will run with mockito as an extension, which allows you tomock
Some of our runtime objects.
@InjectMocks
private UdpCollectImpl udpCollect;
@InjectMocks
alsomockito
This library provides annotations that are typically used to declare classes that need to be tested.
@InjectMocks
private AbstractCollect udpCollect;
Note that this annotation must be a concrete class, not an abstract class or interface.
In fact, when we understand his principle we can know exactly why:
When we debug the run we will findudpCollect
object has a value, and if we remove this annotation@InjectMocks
Running it again throws a null pointer exception.
Because it doesn't initialize udpCollect.
rather than using@InjectMocks
After annotation, themockito
The framework automatically gives theudpCollect
injects a proxy object; if it's an interface or an abstract class, the mockito framework has no way of knowing which object to create.
Of course in this simple scenario, we directlyudpCollect = new UdpCollectImpl()
It's okay to conduct tests.
🔥 Work with jacoco to export single-measurement coverage
In IDEA we can use theCoverage
The way in which the operation of theIDEA
Just show our single test coverage in the source code, the green part represents where it actually executes to at runtime.
We can also be found in themaven
Integration in projectsjacoco
, just add a root directory of Add a
plugin
That's all.
<plugin>
<groupId></groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
afterwardsmvn test
A test report will be generated in the target directory.
We can also integrate in GitHub's CICodecov
He will read the test data directly from jacoco and add a test report in the comments section of PR.
require a license fromCodecov
Just add your project's token to the repo's environment variable.
You can refer to this PR for details:/apache/hertzbeat/pull/1985
☀️ more complex single test
Just shown is a very simple scenario, here's a slightly more complex one.
Let's take this single test as an example:
@ExtendWith()
public class RedisClusterCollectImplTest {
@InjectMocks
private RedisCommonCollectImpl redisClusterCollect;
@Mock
private StatefulRedisClusterConnection<String, String> connection;
@Mock
private RedisAdvancedClusterCommands<String, String> cmd;
@Mock
private RedisClusterClient client;
}
This single test adds one more to the earlier@Mock
The notes.
This is because we need to test theRedisCommonCollectImpl
Classes that require a dependency on theStatefulRedisClusterConnection/RedisAdvancedClusterCommands/RedisClusterClient
The services provided by these classes.
The single test requires the use of themockito
Create an object of them and inject them into theRedisCommonCollectImpl
In the category.
Otherwise we need to prepare the resources needed for the single test, such as Redis, MySQL, etc. that can be used.
🚤 Simulation behavior
It's not enough to just inject it in, we need to simulate its behavior:
- For example, calling a function can simulate returning data
- Simulating a function call that throws an exception
- Simulating Function Call Time Consumption
Here is an example of the most common simulated function return:
String clusterNodes = ().clusterInfo();
In the source code, you can see that the connectionclusterInfo()
function returns cluster information.
String clusterKnownNodes = "2";
String clusterInfoTemp = """
cluster_slots_fail:0
cluster_known_nodes:%s
""";
String clusterInfo = (clusterInfoTemp, clusterKnownNodes);
(()).thenReturn(clusterInfo);
At this point we can use the().thenReturn()
to simulate the return data of this function.
And one of thecmd
Naturally, simulated returns are also required:
().when(()->((),
())).thenReturn(client);
(()).thenReturn(connection);
(()).thenReturn(cmd);
((())).thenReturn(info);
(()).thenReturn(clusterInfo);
cmd
attributable(()).thenReturn(cmd);
returned, and theconnection
beginning of()
Returned.
It ends up being like a condom.client
It is created in the source code by a static function.
⚡ Simulate static functions
I vaguely remember when I was first introduced tomockito
In the 16-17 year period, simulating calls to static functions was not yet supported, but it is now:
@Mock
private RedisClusterClient client;
().when(()->((),
())).thenReturn(client);
This allows you to simulate the return value of a static function, but only if the returnedclient
require the use of@Mock
Annotation.
💥 Analog Constructors
Sometimes we also need to simulate the constructor so that we can simulate the subsequent behavior of this object.
MockedConstruction<FTPClient> mocked = (,
(ftpClient, context) -> {
().when(ftpClient).connect((),
(()));
(invocationOnMock -> true).when(ftpClient)
.login((), ());
((())).thenReturn(isActive);
().when(ftpClient).disconnect();
});
It is possible to use to perform the simulation, and some of the object's behavior is written directly within this simulation function.
It is important to note that the return of themocked
Objects need to remember to close.
Mock not required
Of course not all scenarios requiremock
。
For example, in the first scenario, there is no need for themock
。
Something like this.PR If you're relying on a basic memory cache component, there's no need to mock it, but if you're relying on theRedis
The caching component still needs to be mocked.
/apache/hertzbeat/pull/2021
⚙️ modify source code
If some test scenarios require access to internal variables for subsequent testing, but the test class doesn't provide a function to access the variables, we'll have to modify the source code to match the test.
Like this one.PR:
Of course if it's just for functions or variables that are used in a test environment, we can add the@VisibleForTesting
The annotation is labeled as such, and this annotation serves no other purpose and makes it clearer to subsequent maintainers what this is for.
📈 Integration Testing
Unit testing can only test some of the functions of a single function, to ensure the quality of the entire software only rely on a single test is not enough, we also need integration testing.
Integration testing is usually required for open source projects that need to provide services to the public:
- Pulsar
- Kafka
- Dubbo, etc.
The service-oriented applications I've come across fall into two main categories: Java applications and Golang applications.
🐳Golang
Golang
Because the toolchain is not as robust as Java, most of the integration testing functionality is achieved by writing Makefiles and shell scripts.
I'm still familiar with Pulsar'sgo-client
For example, its GitHub integration test is triggered by a GitHub action, defined as follows:
The final call is to the test command in the Makefile, passing in the version of Golang you want to test.
Dockerfile
:
This image is simply a way to run the Pulsar image as a base image (which contains the Pulsar server), and then copy the code for pulsar-client-go into it and compile it.
Then run:
cd /pulsar/pulsar-client-go && ./scripts/
Also known as a test script.
The logic of the test script is also simple:
- Start the pulsar server
- Run the test code
This is because the address of the connection to the server in all the test code islocalhost
, so it can be directly connected.
Through hereaction Logs keep track of all runs.
☕Java
Java, because of its powerful toolchain, requires little in the way of Makefiles and scripts to execute integration tests.
Or take Pulsar as an example, its integration tests need to simulate starting a server locally (because Pulsar's server source code and test code are written in Java, it is more convenient to do the test), and then run the test code.
The advantage of this is that any single test can be run directly locally, whereas Go code still needs to start a server locally first, which is more cumbersome to test.
To see how it's done, I take one of theBrokerClientIntegrationTestAs an example:
Will start the server side first when the single test starts.
will eventually call thePulsarTestContext
(used form a nominal expression)build
function launchbroker
(server-side), and performing a single test also requires only the use of themvn test
It is possible to trigger these unit tests automatically.
It's just that each single test requires starting and stopping the server, so it usually takes 1 to 2 hours to run all the single tests for Pulsar.
These are the scenarios that you may come across in the day-to-day writing of a single test, and I hope they are helpful.