Static method, mock or no mock, this is a question

Keywords: Java Maven Junit Apache

King Mockito

I don't know when to start. Mockito It has become the king of Java's unit testing framework. At present (July 2019), the star number on Github is approaching 10K. Look at other unit testing tools: PowerMock 2K (no doubt with Mockito light), easymock 600, JMockit 300. Compared with Mockito, it's pitiful that none of them can fight.

Mockito is certainly good. I've been using Mockito since 2012 or 2013, and I've watched it go from 1.0x to 3.0 later this year. There should be a lot of people who have had similar experiences with me, starting with Mockito, touching mock / stub, admiring the simplicity of Mockito grammar and enjoying the convenience of unit testing brought about by mock. Generally speaking, unit testing should isolate external dependencies and implementations. It's hard to imagine how to write unit testing without mock.

public void test() {
    when(userDao.update(any(User.class))).thenReturn(1);
    int actual = userService.update(aUser);
    Assert.assertTrue(acutal > 0);
    verify(userDao).update(aUser);
}

Look at the example of Mockito above, when(...).thenReturn(...), verify(...).doSomething(), this code is like human language, how simple and easy to understand!

However, since 2019, Mockito still does not support mock static methods, construction methods, etc. You can say that this is the design concept, Mockito home page has been written a sentence "Don't mock everything", that should do a good job of functional code design, try to avoid static methods, etc., try to make your code easy to test. This idea, in theory, is no problem, but so many years of development experience tells me that the ideal, in fact, you have to maintain the legacy code is always a basket of baskets, unavoidable.

Static methods, to mock or not to mock, that is a question

Whether to use mock static method in unit testing has been debated for a long time. One One Another There are all kinds of opinions in the discussion.

My personal opinion, follow This view In the same way, I think that test tools should not decide for users what is good and what is bad, but should try to provide choices so that users can make their own decisions and adopt appropriate solutions. The theory is good, but the reality is that google searches for "mockito how to mock static methods" with nearly 150,000 results. It is conceivable how much time developers around the world have wasted on this issue.

To use Mockito to to mock static methods, they are usually combined with PowerMock. I'm not sure how PowerMock has developed in the past two years, but I used PowerMock in 14 or 15 years. I feel like it's fucking tired. In theory, it is possible, but in practice, it is always a variety of problems, then a variety of google, solve, and then continue to various problems, the investigation of my life is almost doubtful. Eventually, I gave up PowerMock, and it's hard to say how many pits there are in the future if I tried so hard to combine the two tools.

Tools such as Mockito and EasyMock do not support mock static methods, in principle because they are based on cglib and can only mock by creating subclasses or implementing interfaces. Is there no mock implementation other than cglib? Of course. Modify the bytecode!

JMockit

Unlike most other unit testing tools implemented using cglib, JMockit Using JDK6's java.lang.instrument package and ASM, the bytecode is dynamically modified at runtime to achieve "Mock Anything". What static methods, constructors, mock whenever you want. A JMockit solves the problem that neither Mockito + PowerMock can solve. Why not use JMockit? Why can't JMockit be popular?

public class UserServiceTest {

    @Tested
    private UserService userService;
    @Injectable
    private UserDao userDao;

    public void test() {
        new Expectations() {
            {
                userDao.update(withInstanceOf(User.class));
                result = 1;
            }
        };

        int actual = userService.update(aUser);
        Assert.assertTrue(acutal > 0);

        new Verifications() {
            {
                userDao.update(withInstanceOf(User.class));
            }
        };
    }
}

The more powerful JMockit is not popular, and I think one of the reasons is that its grammar is not very friendly. Look at the example of JMockit above. What are the ghosts of new Expectations(){...} and new Verifications(){...}? Anonymous classes? Why is there another brace in it? Not to mention test code, we rarely see such grammar in common functional code. Most people may feel unaccustomed and then stop and give up JMockit.

This syntax of JMockit is based on it. record-replay-verify Model. new Expectations() are recording expectations, and new Verifications() are validations, with playback in between - normal invocation of business methods. In the middle of the anonymous inner class, the bracket is Java's Instance Initialization Blocks. We usually use "static initialization blocks" more often, and "instance initialization blocks" are really rare. One of its purposes is to initialize anonymous inner classes because of anonymity. The inner class of a name cannot have a constructor. After understanding these grammars, JMockit is not difficult to understand, and its usage is similar to other test frameworks in that it is more powerful.

Another reason why JMockit is not popular enough, I guess it's about the community. No way, Mockito is so popular, the community is hot and there are a lot of contributors. On the contrary, JMockit, although open source, only the original author Rogério Liesenfeld Oneself in the development and maintenance. This kind of single-person maintenance project may stop one day, and everyone will have this worry. I'm worried, too, but look at JMockit's in recent years. release notes Basically, it will be released every month or two, and it will be scheduled for the next release in advance. I really want to say to the author: Brother, be steady! ___________. So, at least for now, JMockit's stability and activity need not be worried, after all, there is such a stable author.

Configuration example of JUnit5 + JMockit + Surefire + Jacoco

Want to be comfortable with Junit5 and JMockit, as well as unit test coverage? There are still some pits to tread on. In Maven's case, there are several points to note:

  • The default version of maven-surefire-plugin (running unit tests) does not support JUnit 5, so you have to specify the new version number manually.
  • If you want to get unit test coverage with jacoco-maven-plugin, because jacoco also uses a bytecode modification scheme, the default configuration will conflict with JMockit. Some additional configurations are needed, referring specifically to the following examples.

Complete Maven configuration example:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.github.renial</groupId>
    <artifactId>java-utils</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>java-utils</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>

        <jmockit.version>1.46</jmockit.version>
        <jupiter.version>5.4.2</jupiter.version> <!-- Don't use it. junit.version!Otherwise, it may affect other uses. junit4 library -->
        <surefire.version>2.22.2</surefire.version> <!-- Specified version to support JUnit5 -->
        <jacoco.version>0.8.4</jacoco.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.jmockit</groupId>
            <artifactId>jmockit</artifactId>
            <version>${jmockit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${jupiter.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire.version}</version>
                <!-- Individually designated jmockit and jacoco Two agent,Supporting operation jmockit Testing, support jacoco Coverage -->
                <!-- Reference https://github.com/jacoco/jacoco/issues/193 -->
                <configuration>
                    <argLine>
                        -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar -javaagent:"${settings.localRepository}"/org/jacoco/org.jacoco.agent/${jacoco.version}/org.jacoco.agent-${jacoco.version}-runtime.jar=destfile=${project.build.directory}/jacoco.exec
                    </argLine>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.version}</version>
                <executions>
                    <!-- prepare-agent this goal,The original effect is to take it with you. -javaagent:*** Parameters to specify jacoco agent -->
                    <!-- However, maven-surefire-plugin Manual configuration jacoco Of agent After that, here's the prepare-agent Actually, it won't work. -->
                    <!-- However, maven-surefire-plugin Quoted jacoco jar Packet, need to run this once prepare-agent Of goal Only then -->
                    <!-- So, let's configure it. It's no harm. -->
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Posted by byrt on Mon, 29 Jul 2019 22:09:45 -0700