Practice thinking (junit5 + jmockit + testcontainer) unit test

background

Before finishing an article, based on (SpringCloud + Junit5 + Mockito + DataMocker ) frame sorting unit tests. At that time the project is a services orchestration layer, so there is no problem involves complex databases or other middleware. And the project is the beginning, not complex code environment, the architecture was basically able to meet the demand.
Recently in an older project, now we hope to strengthen the quality of code project, so began to introduce unit testing framework. Ever since first introduced in accordance with the original design of the entire framework junit5, while introducing h2 database for simulation and mock the service rabbitmq. This project uses SpringCloud Alibaba framework, service registry and configuration management using nacos, not too many other special places. But the actual writing process, we found some problems:

  • Mock framework uses Mockito and PowerMock, developers need to use both frameworks.
  • H2 database and the actual Mysql database, there are some differences compared to the situation can not support such functions, etc.
  • Data preparation unit testing is relatively complex, it affects how well different unit testing isolation is a problem.
  • Unit testing is to have coverage or for quality assurance of strength, how to improve test quality R & D personnel in the unit.

Design

In response to these problems, we have to solve one by one.
The first is for Mock framework that can be selected Jmockit framework, common methods and static methods can meet directly, but the syntax is relatively good as Mockito natural learning curve is relatively high after the inspection. But eventually decided to try to do a unified framework, reduce the complexity of architecture.
Followed by database problems, there are two options, one is to improve the H2 database, you can use a custom function characteristics to support the missing, but the drawback is also very clear, H2 is not always true Mysql database. Found TestContainer second embodiment, which is a Java library Docker operation, Java code may be utilized directly generated image Docker container and running, so there is a direct way to start Mysql container means for testing directly after completely destroyed. The disadvantage of this approach is that environmental issues, all need to run the unit test environment need to install Docker support, contain their own R & D and CI environment. But the advantage is that a universal middleware simulation program, subsequent Redis, MQ are fully or other middleware may be used to simulate such a scheme.
Data preparation, the problem we set up two ways of data preparation. The first part is to initialize the database, import basic script, the script contains this part of the structure and data is the basis of the data content of all common unit tests need to rely on, such as company, department, staff, roles, permissions and so on. The second part is a single class initialization unit testing, the introduction of the script data, data for only a single unit test class / method, the method will roll back after running, without affecting the operation of other unit tests.
Finally, the strength of the unit test, mainly of some specifications, such tests require that all units must have asserted, and asserts the condition that the content of the data field is reasonably authenticated. You can refer to a write valuable unit testing .
So eventually it will be the framework is Junit5 + Jmockit + TestContainer.

Unit testing guidelines

Prior to the underlying framework set up, we can talk about how to write the real value of unit testing, rather than simply the performance of unit test coverage in order?
Before the period mentioned in the write valuable unit testing and Ali Java code mentioned in the statute at some point

Ali cited the statute:

  1. [Forced] a good unit tests must comply with the principle of AIR.
    Description: When running the unit tests online, feels like air (AIR) as does not exist, but on the quality assurance testing,
    but it is very critical. On a good unit test macro level, with automated, independence, characteristics can be repeated. A: Automatic (Automation) I: Independent (Independence) R: Repeatable (repeat)
  2. [Force] unit tests should be performed fully automatic and non-interactive. Test is usually performed on a regular basis, the Executive
    line must be completely automated process makes sense. The output test requires manual inspection is not a good unit test. Unit
    testing using System.out allowed to verify the human flesh, must assert to verify.
  3. [Forced] to maintain the independence of the unit tests. To ensure stable and reliable unit testing and easily maintained between unit test
    must not call each other, we can not rely on the order of execution.
    Anti Example: performing method2 need to rely on the method1, method2 as input the result of execution.
  4. [Mandatory] unit tests can be performed repeatedly, it can not be affected by the external environment.
    Description: Unit testing will usually be placed in continuous integration, every time when the code check in unit tests will be performed. As
    if a single measurement with a dependence on the external environment (network services, middleware, etc.), not easily lead to a continuous integration mechanism.
    Positive Example: In order to influence from the external environment, requires the designer to put the code into the SUT dependency injection, when the spring test
    injection a local (memory) DI frame such implementations or Mock implemented.
  5. [Force] for unit testing, the test to ensure a sufficiently small particle size to help pinpoint the problem. Measuring a particle size of at most a single class level
    not, the method is generally level.
    Description: Only small size test to locate the error in the error location as soon as possible. Single measurement is not responsible for checking cross-class or cross-system
    interaction logic, it is an area of integration testing.

Some of these ideas will determine the specific implementation in the code unit testing. Then we tried, there are two different implementations in accordance with the above guidelines.

  • Single-isolation
  • Internal penetration

Then we explained in two ways.

Single-isolation

Code will be divided into normal layered controller, service, dao, etc., in a single layer in the isolation of thought, each unit test is made for the code of each layer does not penetrate down. Such wording is mainly to ensure single business logic and properly cured.
In practice, for example, the unit tests the controller layer write controller class code corresponding to the external file all the mock all calls, including internal / external service corresponding. Other layers of code as well.

The advantage of doing this:

  • Unit test code is extremely lightweight, fast. Since only a single internal logic to ensure that the correct class, all the other mock, mock middleware so you can give up, even Spring injection can give up, focus on writing unit tests of logic verification. Such operation is completed package unit test code should also chronograph seconds wheel, relatively complete initialization Spring container may need 20 seconds.
  • In line with the principle of true unit tests can be run in the case of off network. Single-layer logic can shield the service registration and configuration management, the impact of various middleware.
  • Means higher test quality. For single-layer logic verification and assertion to be more clear, if you want to cover a multi-layer, it may ignore the missing link among the various authentication, if coupled with the condition may be the size of a Cartesian product is too complex.

There is also a drawback:

  • Larger than the unit test code, because each individual write unit for testing, but also out of the mock is more dependent on the outside.
  • The learning curve is relatively high, since the programmer is customary for the cell test output given input validation. So there is no output underlying logic for a simple verification process there is a transition on a thinking.
  • For projects of low complexity relatively unfriendly. If your project is mostly CRUD after a simple hierarchical, verifiable fact that unit testing is not too much stuff. However, if the code which is executed complex logic, such wording will be able to play a better quality assurance.

In this project, we did not adopt this method, instead of using a penetrating way. Scene of the project staff, the complexity of the situation, I think this way is not very appropriate.

Internal penetration

Penetration, nature is always call from the top to the bottom. Why plus internal word? In addition to the method that can penetrate within the project, the project still have to rely on external mock out.
Practice, is to write unit tests for controller layer, but will complete call service, dao, the final results are verified on the ground.

advantage:

  • Code amount is relatively small, since it is covered with multiple layers of penetration of the code requires only a test unit to verify from the top.
  • Low learning curve, penetrating prefer black box testing unit, configured developer input conditions, and the results from the floor (storage, such as a database) to verify the expected results.

Disadvantages:

  • Overall heavier, start Spring container, middleware mock, overall unit test run is expected to take minutes is required level. So basic it is to be executed when the CI.

Technical realization

After finalizing the technical program that we can achieve, which is a Java project, use the Maven dependency management. Next we divided into three parts introduction:

  • Dependency Management
  • Infrastructure
  • Examples of realization

Dependency Management

Management relies first point to note, due to the current Junit4 also occupy more market, we have to try to exclude some tests related to dependence contains a reference to the 4.
I posted the first part of next Pom file and associated unit tests

        <!-- Jmockit -->
        <dependency>
            <groupId>org.jmockit</groupId>
            <artifactId>jmockit</artifactId>
            <version>1.49</version>
            <scope>test</scope>
        </dependency>

        <!-- junit5 框架 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.6.1</version>
            <scope>test</scope>
        </dependency>

        <!--  Spring Boot 测试框架 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <!-- exclude junit 4 -->
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--  公司内部封装的一个数据自动Mock框架,来源于Jmockdata -->
        <dependency>
            <groupId>cn.vv.service.unittest</groupId>
            <artifactId>vv-data-mocker</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>test</scope>
        </dependency>

        <!--  testcontainers对于mysql的封装包,当然也可以将mysql替换为testcontainers,这样直接引入底层容器包 -->
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>mysql</artifactId>
            <version>1.12.0</version>
            <scope>test</scope>
        </dependency>

        <!--  testcontainers 容器对于junit5的支持 -->
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>1.12.0</version>
            <scope>test</scope>
        </dependency>

The introduction of these is dependent on the basic, which also needs to be noted that the surefire plugin configuration

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M4</version>
                <configuration>
                    <argLine>-javaagent:${settings.localRepository}/org/jmockit/jmockit/1.49/jmockit-1.49.jar
                        -Dfile.encoding=UTF-8 -Xmx1024m
                    </argLine>
                    <enableAssertions>true</enableAssertions>
                    <!-- <useSystemClassLoader>true</useSystemClassLoader>-->
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.apache.maven.surefire</groupId>
                        <artifactId>surefire-api</artifactId>
                        <version>3.0.0-M4</version>
                    </dependency>
                </dependencies>
            </plugin>

Note the point here is Jmockit need javaagent to initialize JVM parameters.

Infrastructure

Part of the infrastructure, I think is divided into three terms:

  • Unit test group, a package of some basic Mock objects and items common methods used
  • Unit test configuration related
  • TestContainer package

In fact, after these three points are, speak their own separate implementations associated with unit testing base class, will eventually give the complete code.

Package Junit5 & Jmockit

First, annotation notes section 5 Junit4 to adjust and change, and our project is based SpringCloud, so the final unit testing base class BaseTest uses three notes

@SpringBootTest(classes = {OaApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Transactional
@Slf4j

The head of the class is Junit5 do not need any annotations, and mainly with Spring, we used SpringBootTest provide annotation Boot Test, specifying the start of class entry, in order to include the configuration file, get nacos configuration.
Transaction data annotation method is to allow the operation can be rolled back, does not affect other unit tests.
The final step is lombok log notes.

The next step is BeforeAll, AfterAll, BeforeEach, AfterEach a few notes.
The idea here is to use Jmockit, treat the underlying mechanisms within the test system business unified Mock treatment, such as the head of information request or session. Here's the code I could and we each project are more differences, just a thought. Mock Jmockit to use static methods getting some of our objects, the object directly returns the result of our design.


    @BeforeAll
    protected static void beforeAll() {

        new MockUp<ShiroUtils>(ShiroUtils.class) {
            @Mock
            public EmployeeVO getEmployee() {
                EmployeeVO employeeVO = new EmployeeVO();
                employeeVO.setUserName("mock.UserName");
                employeeVO.setUserNo("mock.UserNo");
                employeeVO.setCompanyName("mock.CompanyName");
                employeeVO.setDepartmentName("mock.DepartmentName");
                return employeeVO;
            }
        };
        new MockUp<LogAspect>(LogAspect.class) {
            @Mock
            public String getIp() {
                return "mock.ip";
            }
        };
    }

    @AfterAll
    protected static void destroy() {
    }

    @BeforeEach
    protected void beforeEach() {

        new MockUp<WebUtil>(WebUtil.class) {
            @Mock
            public HttpServletRequest getRequest() {
                return getRequest;
            }

            @Mock
            public VvCurrentAccount getCurrentAccount(Boolean isMustLogin) {
                VvCurrentAccount vvCurrentAccount = new VvCurrentAccount();
                vvCurrentAccount.setUserCode("mock.userCode");
                return vvCurrentAccount;
            }
        };
        new MockUp<ServletUtils>(ServletUtils.class) {
            @Mock
            public HttpServletRequest getRequest() {
                return getRequest;
            }
        };


        if (StringUtil.isNotBlank(this.getDbScript())) {
            try {
                ScriptRunner runner = new ScriptRunner(dataSource.getConnection());
                runner.setErrorLogWriter(null);
                runner.setLogWriter(null);
                runner.runScript(new FileReader(this.getClass().getResource(this.getDbScript()).getPath()));
            } catch (Exception e) {
                log.error("ScriptRunner error!", e);
            }
        }
    }

    @AfterEach
    protected void afterEach() {
    }


    protected String getDbScript() {
        return "";
    }

There is a design point can discuss, beforeEach called a getDbScript, data used to build a single unit test class required before the unit test method. And because the class inherits the default transaction rollback, so this operation is completed the data will be rolled back after the end of the process, so the impact of data to a minimum.
Each unit test classes as long as the rewrite getDbScript method, provide their own database script. Such a test method designed to isolate the data unit level.

Test Configuration Unit

Since the frame of this project uses Nacos, and its address space is in Pom configuration file, specify the Profile in run time to invoke configure different environments. During normal use, information access addresses, user passwords and other middleware is saved on Nacos, due to the need to run the unit tests Mock true middleware, so all the information needs to be replaced.
The first version is the use of their own characteristics Nacos, unit test head used @ActiveProfile("")later, reads the configuration file corresponding to the properties to replace the placeholder, such as the original is written in our configuration vv-oa.yml we specifies ActiveProfile ( "test"), will be to load vv-oa-test.properties file, used to replace yml configuration.
By this method to achieve the purpose of replacing only intermediate connection unit tests.
However, due to the use of middleware Mock method TestContainer, in fact, can not directly address container is fixed, so the program is not very appropriate. By using the form (AutoConfiguration) locally configured, create a new profile based on the unit test package.


@Configuration
@EnableTransactionManagement
public class JunitDataSource {

    @Bean
    public DataSource dataSource() throws Exception {
        Properties properties = new Properties();
        properties.setProperty("driverClassName", System.getProperty("spring.datasource.driver-class-name"));
        properties.setProperty("url", System.getProperty("spring.datasource.url"));
        properties.setProperty("username", System.getProperty("spring.datasource.username"));
        properties.setProperty("password", System.getProperty("spring.datasource.password"));
        return DruidDataSourceFactory.createDataSource(properties);
    }

    @Bean
    public PlatformTransactionManager transactionManager() throws Exception {
        return new DataSourceTransactionManager(dataSource());
    }

}

Other middleware also be used in the same way.

TestContainer package

First, to provide you the official website and their Github code sample library , a lot of usage is a reference to the official. In this paper, a container as Mysql examples give you a brief introduction to use.

The official program

In the official document container database section, we describe two ways to use the database container:

  • Start container code
  • Start container through JDBC url
   @Rule
    public MySQLContainer mysql = new MySQLContainer();

Start the code is so simple, a simple Mysql container starts, the default configuration information is as follows:

    public static final String NAME = "mysql";
    public static final String IMAGE = "mysql";
    public static final String DEFAULT_TAG = "5.7.22";
    private static final String MY_CNF_CONFIG_OVERRIDE_PARAM_NAME = "TC_MY_CNF";
    public static final Integer MYSQL_PORT = 3306;
    private String databaseName = "test";
    private String username = "test";
    private String password = "test";
    private static final String MYSQL_ROOT_USER = "root";

Then in BeforeAll call mysql.start(), the container will be started.

JDBC simpler manner, without any code in the direct drive configuration and specified to url

spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.url=jdbc:tc:mysql:5.7.22:///databasename?TC_INITSCRIPT=file:src/main/resources/init_mysql.sql&TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction

Here are several points to note

  • The driver must use the supplied tc
  • url in mysql with the time after the version number, correspondence can be understood as dockerhub in mysql mirrored version is actually the actual version of mysql.
  • tc, then the initial database provides two ways, directly specifying script TC_INITSCRIPT , or the specified class initialization code TC_INITFUNCTION , these two methods can exist simultaneously .

The actual program

When used in the project are two ways above actually it is not very good, MySQLContainer after a relatively small package customized content, JDBC way is the same problem such as port configuration, etc. can not be set.
For greater flexibility, we use the most primitive base container class to build yourself a Mysql container. First given directly code.


    @ClassRule
    public static GenericContainer mysql = new VvFixedHostPortGenericContainer(
            new ImageFromDockerfile("mysql-vv-gms")
                    .withDockerfileFromBuilder(dockerfileBuilder -> {
                        dockerfileBuilder.from("mysql:8.0.0")
                                .env("MYSQL_ROOT_PASSWORD", "test")
                                .env("MYSQL_DATABASE", "test")
                                .env("MYSQL_USER", "test")
                                .env("MYSQL_PASSWORD", "test")
                                .add("my.cnf", "/etc/mysql/conf.d")
                                .add("db-schema.sql", "/docker-entrypoint-initdb.d")
                        ;
                    })
                    .withFileFromClasspath("my.cnf", "my.cnf")
                    .withFileFromClasspath("db-schema.sql", "db-schema.sql")
    )
            .withFixedExposedPort(3307, 3306)
            .waitingFor(Wait.forListeningPort());
package cn.vv.oa.init;

import lombok.NonNull;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.InternetProtocol;

import java.util.concurrent.Future;

public class VvFixedHostPortGenericContainer<SELF extends VvFixedHostPortGenericContainer<SELF>> extends GenericContainer<SELF> {

    public VvFixedHostPortGenericContainer(@NonNull final Future<String> image) {
        super(image);
    }

    /**
     * Bind a fixed TCP port on the docker host to a container port
     *
     * @param hostPort a port on the docker host, which must be available
     * @param containerPort a port in the container
     * @return this container
     */
    public SELF withFixedExposedPort(int hostPort, int containerPort) {

        return withFixedExposedPort(hostPort, containerPort, InternetProtocol.TCP);
    }

    /**
     * Bind a fixed port on the docker host to a container port
     *
     * @param hostPort a port on the docker host, which must be available
     * @param containerPort a port in the container
     * @param protocol an internet protocol (tcp or udp)
     * @return this container
     */
    public SELF withFixedExposedPort(int hostPort, int containerPort, InternetProtocol protocol) {

        super.addFixedExposedPort(hostPort, containerPort, protocol);

        return self();
    }
}

The second fact can not VvFixedHostPortGenericContainer particular concern, the class designated port is exposed only to the container base class methods, and by constructing Dockerfile image generated constructor. The key is to see part of the first paragraph of the statement mysql container.
withDockerfileFromBuilder this method as it is specified constructor Dockerfile, the method can expose command Dockerfile are able to write, if you know it's good docker customized way. Add command files which can be added, we need to come back with withFileFromClasspath map.
Will start the two ports 3306 and 33060 after exposure to specify the port, mysql8 by withFixedExposedPort method, we only need to be exposed to 3306.
Add the two files here also need to find out.
my.cnf file to override the default configuration of mysql, database coding can solve the underlying problem is set to be noted that the file path to add commands to add /etc/mysql/conf.d way to the initial configuration.
db-schem.sql is to initialize the database scripts, adding in the container /docker-entrypoint-initdb.d path will be performed automatically, but note that you can only add a script.
Incidentally, it is also my.cnf Tieshanglai it might affect Chinese garbled database

[mysqld]
user = mysql
datadir = /var/lib/mysql
port = 3306
#socket = /tmp/mysql.sock
skip-external-locking
key_buffer_size = 16K
max_allowed_packet = 1M
table_open_cache = 4
sort_buffer_size = 64K
read_buffer_size = 256K
read_rnd_buffer_size = 256K
net_buffer_length = 2K
skip-host-cache
skip-name-resolve
character-set-server = utf8
collation-server = utf8_general_ci

# Don't listen on a TCP/IP port at all. This can be a security enhancement,
# if all processes that need to connect to mysqld run on the same host.
# All interaction with mysqld must be made via Unix sockets or named pipes.
# Note that using this option without enabling named pipes on Windows
# (using the "enable-named-pipe" option) will render mysqld useless!
#
#skip-networking
#server-id = 1

# Uncomment the following if you want to log updates
#log-bin=mysql-bin

# binary logging format - mixed recommended
#binlog_format=mixed

# Causes updates to non-transactional engines using statement format to be
# written directly to binary log. Before using this option make sure that
# there are no dependencies between transactional and non-transactional
# tables such as in the statement INSERT INTO t_myisam SELECT * FROM
# t_innodb; otherwise, slaves may diverge from the master.
#binlog_direct_non_transactional_updates=TRUE

# Uncomment the following if you are using InnoDB tables
innodb_data_file_path = ibdata1:10M:autoextend
# You can set .._buffer_pool_size up to 50 - 80 %
# of RAM but beware of setting memory usage too high
innodb_buffer_pool_size = 16M
#innodb_additional_mem_pool_size = 2M
# Set .._log_file_size to 25 % of buffer pool size
innodb_log_file_size = 5M
innodb_log_buffer_size = 8M
innodb_flush_log_at_trx_commit = 1
innodb_lock_wait_timeout = 50

[mysql.server]
default-character-set=utf8
[mysql_safe]
default-character-set=utf8
[client]
default-character-set=utf8

Complete class code

package cn.vv.oa;

import cn.vv.OaApplication;
import cn.vv.fw.common.api.VvCurrentAccount;
import cn.vv.fw.common.utils.StringUtil;
import cn.vv.fw.common.utils.WebUtil;
import cn.vv.oa.api.org.vo.EmployeeVO;
import cn.vv.oa.common.aspectj.LogAspect;
import cn.vv.oa.common.filter.TokenAuthorFilters;
import cn.vv.oa.common.shiro.ShiroUtils;
import cn.vv.oa.common.utils.ServletUtils;
import cn.vv.oa.init.VvFixedHostPortGenericContainer;
import lombok.extern.slf4j.Slf4j;
import mockit.Mock;
import mockit.MockUp;
import mockit.Mocked;
import org.apache.ibatis.jdbc.ScriptRunner;
import org.apache.shiro.authz.aop.PermissionAnnotationHandler;
import org.junit.ClassRule;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.builder.ImageFromDockerfile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.io.FileReader;

@SpringBootTest(classes = {OaApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Transactional
@Slf4j
public class BaseTest {

    @ClassRule
    public static GenericContainer mysql = new VvFixedHostPortGenericContainer(
            new ImageFromDockerfile("mysql-vv-gms")
                    .withDockerfileFromBuilder(dockerfileBuilder -> {
                        dockerfileBuilder.from("mysql:8.0.0")
                                .env("MYSQL_ROOT_PASSWORD", "test")
                                .env("MYSQL_DATABASE", "test")
                                .env("MYSQL_USER", "test")
                                .env("MYSQL_PASSWORD", "test")
                                .add("my.cnf", "/etc/mysql/conf.d")
                                .add("db-schema.sql", "/docker-entrypoint-initdb.d")
                        ;
                    })
                    .withFileFromClasspath("my.cnf", "my.cnf")
                    .withFileFromClasspath("db-schema.sql", "db-schema.sql")
    )
            .withFixedExposedPort(3307, 3306)
            .waitingFor(Wait.forListeningPort());

    @Resource
    protected DataSource dataSource;

    @Mocked
    PermissionAnnotationHandler permissionAnnotationHandler;
    @Mocked
    cn.vv.fw.boot.logger.RequestLogAspect RequestLogAspect;
    @Mocked
    TokenAuthorFilters tokenAuthorFilters;
    @Mocked
    HttpServletRequest getRequest;

    @BeforeAll
    protected static void beforeAll() {
        mysql.start();

        System.setProperty("spring.datasource.url", "jdbc:mysql://" + mysql.getContainerIpAddress() + ":3307/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2b8");
        System.setProperty("spring.datasource.driver-class-name", "com.mysql.cj.jdbc.Driver");
        System.setProperty("spring.datasource.username", "test");
        System.setProperty("spring.datasource.password", "test");

        new MockUp<ShiroUtils>(ShiroUtils.class) {
            @Mock
            public EmployeeVO getEmployee() {
                EmployeeVO employeeVO = new EmployeeVO();
                employeeVO.setUserName("mock.UserName");
                employeeVO.setUserNo("mock.UserNo");
                employeeVO.setCompanyName("mock.CompanyName");
                employeeVO.setDepartmentName("mock.DepartmentName");
                return employeeVO;
            }
        };
        new MockUp<LogAspect>(LogAspect.class) {
            @Mock
            public String getIp() {
                return "mock.ip";
            }
        };
    }

    @AfterAll
    protected static void destroy() {
        mysql.stop();
    }

    @BeforeEach
    protected void beforeEach() {

        new MockUp<WebUtil>(WebUtil.class) {
            @Mock
            public HttpServletRequest getRequest() {
                return getRequest;
            }

            @Mock
            public VvCurrentAccount getCurrentAccount(Boolean isMustLogin) {
                VvCurrentAccount vvCurrentAccount = new VvCurrentAccount();
                vvCurrentAccount.setUserCode("mock.userCode");
                return vvCurrentAccount;
            }
        };
        new MockUp<ServletUtils>(ServletUtils.class) {
            @Mock
            public HttpServletRequest getRequest() {
                return getRequest;
            }
        };


        if (StringUtil.isNotBlank(this.getDbScript())) {
            try {
                ScriptRunner runner = new ScriptRunner(dataSource.getConnection());
                runner.setErrorLogWriter(null);
                runner.setLogWriter(null);
                runner.runScript(new FileReader(this.getClass().getResource(this.getDbScript()).getPath()));
            } catch (Exception e) {
                log.error("ScriptRunner error!", e);
            }
        }
    }

    @AfterEach
    protected void afterEach() {
    }

    protected String getDbScript() {
        return "";
    }

}

Examples of realization

The company's actual interface as an example, our test unit entrance from the Controller method.

package cn.vv.oa.module.org.controller;

import cn.vv.fw.common.api.R;
import cn.vv.oa.BaseTest;
import cn.vv.oa.api.org.dto.CompanyDTO;
import cn.vv.oa.module.org.entity.Company;
import cn.vv.oa.module.org.repository.mapper.CompanyMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.junit.jupiter.api.Test;

import javax.annotation.Resource;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class CompanyControllerTest extends BaseTest {

    @Resource
    CompanyController companyController;

    @Resource
    CompanyMapper companyMapper;

    @Test
    public void getList() throws Exception {
        List dtos = companyController.getList("100", "").getData();
        assertEquals(((Map) (dtos.get(0))).get("companyName"), "VV科技集团");
    }

    @Test
    void getAllList() {
        List<Company> list = companyMapper.selectList(new LambdaQueryWrapper<Company>());
        assertEquals(list.size(), 3);
    }


    @Test
    void saveOrUpdate() throws Exception {
        CompanyDTO companyDTO = CompanyDTO.builder()
                .companyName("VV日本公司")
                .parentId(new BigInteger("100"))
                .companyEmail("[email protected]")
                .companyArea(Arrays.asList("Japan"))
                .regTime(LocalDate.now())
                .build();

        R r = companyController.saveOrUpdate(companyDTO);

        List<Company> list = companyMapper.selectList(new LambdaQueryWrapper<Company>());
        assertEquals(list.size(), 4);

    }
}

This unit tests the code cover layers controller, service, dao's. Spring can be seen by the responsible use of injected or original way.
It is noted here that the point to be tested after the unit testing method call, since we need to verify the data through the floor, so it needs a corresponding injection Mapper directly on the database search. This point will be some around or not directly.
This is an example of a penetration. Let us look at an isolated example.


    @Test
    void save() {
        R<AccountSimpleVO> r = new R<>();
        AccountSimpleVO accountSimpleVO = new AccountSimpleVO();
        accountSimpleVO.setUserCode("usercode");
        r.setCode(ResultCode.SUCCESS.getCode());
        r.setData(accountSimpleVO);

        new Expectations() {{
            userMapper.selectList((Wrapper<User>) any);
            result = null;

            userClient.getUserInfo((AccountDTO) any);
            result = null;

            userClient.registered((AccountDTO) any);
            result = r;

            companyMapper.selectOne((Wrapper<Company>) any);
            Company company = new Company();
            company.setCompanyArea("中国");
            result = company;
        }};

        new MockUp<DictUtil>(DictUtil.class) {
            @Mock
            public Map<String, DictDTO> getDictNameMap(String code) {
                Map<String, DictDTO> r1 = new HashMap<>();
                DictDTO dictDTO = new DictDTO();
                dictDTO.setRemark("30");
                r1.put("美国", dictDTO);
                return r1;
            }

            @Mock
            public Map<String, DictDTO> getDictMap(String code) {
                Map<String, DictDTO> r2 = new HashMap<>();
                DictDTO dictDTO = new DictDTO();
                dictDTO.setRemark("86");
                r2.put("中国", dictDTO);
                return r2;
            }

        };

        Assertions.assertThrows(NullPointerException.class, () -> {
            employeeService.save(new EmployeeDTO());
        });
    }

This example is a service individual test methods, you can see mock a lot of internal and external services, including the underlying mapper are mock the content means that the data have been completely read returned quarantined.

to sum up

Unit testing, we have a consensus is one of the most important means of code quality, but we need to really " valuable " unit tests. Truly valuable means to maintain the quality of projects, research and development can make a real willingness to spend energy to write and maintain test cases. If you look at the company unit test coverage, it is actually very good fool, and this becomes the face and has no value. R & D to write unit tests only for performance, high coverage, does not contribute to improve the quality of projects.
If you're reading this article you are a Leader, you must be a personal battle to lead the team to seriously implement, guide the team to really understand the value of writing unit tests.
Our team is also still going to try, in our tests, generating valuable unit testing, code amount is 2-3 times the actual business code. And when the business is unstable, maintenance service code also lead to modifying unit test code, change the code efficiency is to write half the code efficiency, the cost is very high.

Guess you like

Origin www.cnblogs.com/pluto4596/p/12610333.html