5 Essential Practices to Improve Development Quality

unit test

What is unit testing?

Unit testing usually refers to testing a function or method. The purpose of unit testing is to verify that each unit behaves as expected and to be able to quickly detect any potential problems when modifying the code. By writing test cases, we can verify that these modules produce the correct output given a specific input. The purpose of unit testing is to make sure that each module works correctly under various circumstances.

The benefits of writing unit tests

Can bring the following benefits:

  1. Improve code quality: Unit testing allows us to discover potential problems in the code in advance, such as boundary conditions, abnormal conditions, etc., thereby reducing the probability of errors.
  2. Improve code maintainability: Unit testing can help developers understand the functionality and implementation details of the code, making it easier to maintain and modify the code.
  3. Improve code reliability: After the code is modified, unit testing can help developers verify the correctness of the code, thereby improving the reliability of the code.

Writing unit tests is a good software development practice that can improve code quality, maintainability, and reliability, as well as improve development efficiency and support continuous integration and continuous delivery.

Getting started with unit testing

To get started with unit testing, we usually start with static testing (Static Test) at the same time, because it is simple and easy to understand. Static testing (Static Test) means that when writing test cases, we define all the test methods and test data in advance. These test methods and data have been determined at compile time and will not change at runtime. Static tests in Junit usually have regular annotations such as @Test, @Before, @After, etc. Let's start with a set of simple static test examples.

First, make sure your  pom.xml file includes the JUnit dependencies:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.8.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.8.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Then, create a simple calculator class, usually replaced with the business class you actually want to test:

public class SimpleCalculator {

    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }
}

Then  /test create the corresponding test class in the same directory as

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

public class SimpleCalculatorTest {

    // 在所有测试方法执行前,仅执行一次。这个方法需要是静态的。
    @BeforeAll
    static void setup() {
        System.out.println("BeforeAll - 初始化共享资源,例如数据库连接");
    }

    // 在所有测试方法执行后,仅执行一次。这个方法需要是静态的。
    @AfterAll
    static void tearDown() {
        System.out.println("AfterAll - 清理共享资源,例如关闭数据库连接");
    }

    // 在每个测试方法执行前,都会执行一次。用于设置测试方法所需的初始状态。
    @BeforeEach
    void init() {
        System.out.println("BeforeEach - 初始化测试实例所需的数据");
    }

    // 在每个测试方法执行后,都会执行一次。用于清理测试方法使用的资源。
    @AfterEach
    void cleanup() {
        System.out.println("AfterEach - 清理测试实例所用到的资源");
    }

    // 标注一个测试方法,用于测试某个功能。
    @Test
    void testAddition() {
        System.out.println("Test - 测试加法功能");
        SimpleCalculator calculator = new SimpleCalculator();
        assertEquals(5, calculator.add(2, 3), "2 + 3 应该等于 5");
    }

    // 再添加一个测试方法
    @Test
    void testSubtraction() {
        System.out.println("Test - 测试减法功能");
        SimpleCalculator calculator = new SimpleCalculator();
        assertEquals(1, calculator.subtract(3, 2), "3 - 2 应该等于 1");
    }
}

In the above program, you can see the instructions for using common annotations of Junit:

  • @BeforeAll: Executed only once before all test methods are executed. This method needs to be static
  • @AfterAll: Executed only once after all test methods are executed. This method needs to be static
  • @BeforeEach: Before each test method is executed, it will be executed once. Used to set the initial state required by the test method
  • @AfterEach: After each test method is executed, it will be executed once. Used to clean up resources used by test methods
  • @Test: Annotate a test method for testing a function

If it is a maven project, you can execute the command to execute the test in the directory:

mvn test

Output result:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running SimpleCalculatorTest
BeforeAll - 初始化共享资源,例如数据库连接
BeforeEach - 初始化测试实例所需的数据
Test - 测试加法功能
AfterEach - 清理测试实例所用到的资源
BeforeEach - 初始化测试实例所需的数据
Test - 测试减法功能
AfterEach - 清理测试实例所用到的资源
AfterAll - 清理共享资源,例如关闭数据库连接
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.058 s - in SimpleCalculatorTest
[INFO] 
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

The above is a simple example of static testing

Dynamic Testing

Dynamic Test (Dynamic Test): Dynamic testing means that when writing test cases, we can generate test methods and test data at runtime. These test methods and data are not determined at compile time, but are dynamically generated at runtime based on specific conditions or data sources. Because in static unit tests, due to the limited test sample data, it is usually difficult to cover all situations, and it is difficult to improve the coverage rate when it reaches a critical value. Dynamic testing introduced in JUnit 5  is more complex than static testing, and of course more flexible, and more suitable for complex scenarios. Next, a simple example is used to show the difference between dynamic testing and static testing . We create  MyStringUtil a class that has a method  reverse() for reversing strings, as follows:

public class MyStringUtil {
    public String reverse(String input) {
        if (input == null) {
            return null;
        }
        return new StringBuilder(input).reverse().toString();
    }
}

In the static test class, we use  @Test the definition of 3 methods to try to cover  reverse() possible multiple situations:

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

public class MyStringUtilStaticTest {

    private MyStringUtil stringUtil = new MyStringUtil();

    @Test
    void reverseString() {
        // 反转字符串 'hello'
        assertEquals("olleh", stringUtil.reverse("hello"));
    }

    @Test
    void reverseEmptyString() {
        // 反转空字符串
        assertEquals("", stringUtil.reverse(""));
    }

    @Test
    void handleNullString() {
        // 处理 null 字符串
        assertEquals(null, stringUtil.reverse(null));
    }
}

Then implement the same test case with dynamic tests:

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;

import java.util.Arrays;
import java.util.Collection;

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

public class MyStringUtilDynamicTest {

    private MyStringUtil stringUtil = new MyStringUtil();

    // 使用 @TestFactory 注解定义了一个动态测试工厂方法 reverseStringDynamicTests()
    // 工厂方法返回一个 Collection<DynamicTest>
    @TestFactory
    Collection<DynamicTest> reverseStringDynamicTests() {
        // 包含了 3 个动态测试用例,每个测试用例使用 dynamicTest() 方法创建
        return Arrays.asList(
                dynamicTest("动态测试:反转字符串 'hello'", () -> assertEquals("olleh", stringUtil.reverse("hello"))),
                dynamicTest("动态测试:反转空字符串", () -> assertEquals("", stringUtil.reverse(""))),
                dynamicTest("动态测试:处理 null 字符串", () -> assertEquals(null, stringUtil.reverse(null)))
        );
    }
}

The logic in the dynamic test class is as follows:

  1. @TestFactory A dynamic test factory method is defined  using  annotations reverseStringDynamicTests().
  2. The factory method returns a  Collection<DynamicTest>, which contains 3 dynamic test cases.
  3. Each test case  dynamicTest() is created using a method.

The above is the basic unit test usage method. The specific use of Junit 5 is not going to be explained in detail here. If you are interested, you can refer to the official documentation of Junit 5

Unit testing + Dbc

Writing unit tests needs to follow the design by contract (Design By Contract, DbC) code style as much as possible   . For design by contract, please refer to the following description:

Design By Contract (DbC) is a software development method, which emphasizes that for each module or function in software development, the agreement (contract) of its input and output should be clearly defined. These contracts can include preconditions and postconditions, as well as exceptions that may occur. When the code is implemented, these contracts must be satisfied, otherwise an error or exception will be thrown.

It may be abstract to say this, and you can understand how to use assertions to implement contract design through the following sample code:

public class BankAccount {
    private double balance;

    public BankAccount(double balance) {
        this.balance = balance;
    }
    
    public void withdraw(double amount) {
        assert amount > 0 : "Amount must be positive";
        assert amount <= balance : "Insufficient balance";
        
        balance -= amount;
        
        assert balance >= 0 : "Balance can't be negative";
    }
    
    public double getBalance() {
        return balance;
    }
}

In this example, we use assertions in Java to implement Design by Contract. Specifically:

  • assert amount > 0 : "Amount must be positive"; Indicates that the withdrawal amount  amount must be greater than 0
  • assert amount <= balance : "Insufficient balance"; Indicates that the withdrawal amount  amount must be less than or equal to the account balance balance
  • assert balance >= 0 : "Balance can't be negative"; Indicates that after the withdrawal is completed,  balance the value of the account balance should be non-negative

The assertion function can be enabled by using JVM  -ea parameters, but because it is troublesome to enable Java native assertions, the Guava team added a Verify class that is always enabled to replace assertions. They suggest to statically import the Verify method. The usage is similar to assertion, so I won’t go into details here.

Test Driven Development TDD

Test-driven development (TDD) is a software development method, and it is also a software development method that I personally highly recommend, which is to write unit tests before writing code. The core idea of ​​TDD is to write test cases before writing code. Developers think about expected outcomes before writing code so they can write test cases. Developers then write code that is simple enough to pass the test cases, and refactor the code to improve quality and maintainability.

As a long-term practitioner of TDD, I summarize the benefits that TDD can bring as follows:

  1. Improve maintainability: Usually the reason why we dare not maintain a piece of code is that there is no test. The perfect test established by TDD can provide guarantee for refactoring code
  2. Faster development: Many developers always think about implementing the function and then supplementing the test, but usually after the function is implemented, there will be more functions, so try to write the test before the function starts
  3. Higher-quality delivery: Needless to say here, the code that passes the test is completely different from the code that has not been tested. Untested code is not production ready at all

log

Adequate logs can help developers better understand the operation of the program. By looking at the logs, you can understand what happened in the program and where the problem occurred. This helps developers find and fix problems faster, improving program stability and reliability. Additionally, logs can be used to track program performance and behavior for optimization and improvement.

log output

Here is an example of printing a simple log:

1. First, you need to add SLF4J dependencies to your project. You can add the following dependencies in Maven or Gradle:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>

2. Next, you need to choose an implementation of SLF4J, such as Logback or Log4j2, and add it to the project. You can add the following dependencies in Maven or Gradle:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

3. In the code, you can use the following code to print Hello World:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
    private static final Logger logger = LoggerFactory.getLogger(HelloWorld.class);

    public static void main(String[] args) {
        logger.info("Hello World");
    }
}

This will print a message using SLF4J with the string "Hello World". You can view this information on the console or in a log file.

log level

Mainly to help developers better control and manage log output. SLF4J defines several log levels:

log level content
TRACE Used to trace program details, usually for debugging.
DEBUG It is used to debug the program and output detailed information in the program, such as the value of the variable, the call of the method, etc.
INFO Used to output the running status information of the program, such as program startup, shutdown, connection to the database, etc.
WARN It is used to output warning information, indicating that there may be potential problems in the program, but it will not affect the normal operation of the program.
ERROR It is used to output error information, indicating that an error has occurred in the program, including fatal errors.

Different log levels are used to record different information. The purpose of this is not only to reduce unnecessary log output and file size, but also to provide the ability to quickly locate. For example, the development environment usually uses TRACE and DEBUG logs, and the production environment usually uses INFO and WARN logs. These information can  logback.xml be configured in the log configuration file.

log configuration

Here is an example of a basic logback configuration file that outputs logs to the console and to a file:

<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/var/log/myapp.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>/var/log/myapp.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>7</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="FILE" />
  </root>
</configuration>

In this configuration file, two appenders are defined:

  1. One for logging to the console (CONSOLE)
  2. One for logging output to a file (FILE)

The log format of the console uses the pattern format, and the log of the file uses RollingFileAppender to achieve daily rotation, and defines a log history that can be saved for up to 7 days. At the same time, a logger whose root level is INFO is defined, which will output logs to two appenders, CONSOLE and FILE, and other log levels (TRACE, DEBUG, WARN, ERROR) will be output to the root logger according to the default configuration .

code static checking

The Java static scanning tool can help developers discover and fix problems and errors in the code in a timely manner during the development process, thereby improving code quality and security. These static scanning tools can also constrain the code style. In team-assisted development, a unified style can enhance team collaboration and communication, increase code readability and maintainability, and reduce unnecessary discussions and disputes, which is beneficial to Subsequent CodeReview progress. Here are some commonly used Java static scanning tools:

tool name Github address
FindBugs GitHub - findbugsproject/findbugs: The new home of the FindBugs project
PMD https://github.com/pmd/pmd
Checkstyle GitHub - checkstyle/checkstyle: Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard. By default it supports the Google Java Style Guide and Sun Code Conventions, but is highly configurable. It can be invoked with an ANT task and a command line program.
SonarQube GitHub - SonarSource/sonarqube: Continuous Inspection
I understand the idea GitHub - JetBrains/intellij-community: IntelliJ IDEA Community Edition & IntelliJ Platform

Visiting their Github addresses also provides more information and support that can help developers better understand and use these tools. In addition, it is recommended to integrate these tools into the continuous integration and continuous delivery process during the development process to automate code inspection and repair.

Code Review

Manual CodeReview is usually the last step in the development process. Why do so many tests and inspection tools have been done before, and manual inspection is required at the end?

Because static scanning tools can usually only check for some simple problems and errors, compared with manual inspection, it has the following limitations:

  1. Can only check for eg syntax errors, errors common to security holes, etc.
  2. Can only check problems and errors, but cannot give better suggestions and solutions. (the general solution it provides may not be the best)
  3. Static scanning tools can only check whether the code conforms to specific specifications and standards, but cannot ensure the quality and readability of the code.

Compared with machine scanning, manual Code Review can provide the following irreplaceable advantages:

  1. More complex problems can be found, such as: business logic problems, unreasonable design, unnecessary complexity, etc.
  2. Compared with machine suggestions, manual Code Review can provide better solutions and suggestions based on experience and knowledge
  3. It can promote team collaboration and learning. By sharing and discussing code, it can improve developers' skills and knowledge, and improve team cohesion and efficiency.

To sum up, although static scanning tools can help developers automatically find problems and errors in the code, Code Review is still a necessary software development practice that can improve the quality, readability and maintainability of the code, It also promotes teamwork and learning. Therefore, it is recommended to combine manual Code Review and static scanning tools during the development process for more comprehensive and in-depth review and review of code.


Expansion of related fields: (technical frontier)

Shout out! About the current low-code is very active in the technical field!

What is low code? A set of digital technology tool platforms can realize rapid construction, data arrangement, connection ecology, middle-end services, etc. based on more efficient methods such as graphical drag and drop and parameterized configuration. Realize scenario application innovation in digital transformation with little or no code. It can alleviate or even solve the contradiction between supply and demand caused by huge market demand and traditional development productivity, and is a product of the trend of cost reduction and efficiency increase in the process of digital transformation.

Here is an easy-to-use low-code platform-JNPF rapid development platform. In recent years, it has been outstanding in terms of market performance and product competitiveness, and adopts the latest mainstream front-to-back separation framework (SpringBoot+Mybatis-plus+Ant-Design+Vue 3 ) . The code generator has low dependence, flexible expansion capability, and can flexibly realize secondary development.

In order to support application development with higher technical requirements, the enterprise-level low-code platform represented by JNPF has almost no difference from traditional software development from database modeling, Web API construction to page design. For the repetitive work of the "addition, deletion, modification and query" function, partners who have not yet learned about low-code can try to understand it.

Application: https://www.jnpfsoft.com/?csdn

With it, developers can easily get started during the development process and make full use of the experience accumulated in the traditional development mode. Therefore, low-code platforms are of great help to programmers.

Summarize

In modern software development, unit testing, TDD, logging, static check scanning, and manual Code Review are all necessary practices that can help developers ensure software quality, improve code readability and maintainability, and promote team collaboration and learning .

First of all, unit testing is a testing method used to test basic units of code, such as functions, methods, etc. Unit testing can help developers find and solve problems and bugs in the code early, thereby improving code quality and reliability. At the same time, unit testing can also improve the readability and maintainability of the code, making the code easier to understand and modify.

Secondly, TDD (Test-Driven Development, Test-Driven Development) is a development method that requires writing test cases before writing code. By using TDD, developers can better understand code requirements and specifications, avoid errors and problems in code, and improve code readability and maintainability.

Third, the log is a method of recording the status and information of the program when it is running. Logs can help developers debug programs, find potential errors and problems, and provide better error handling and handling schemes. At the same time, the log can also record the performance and status of the program when it is running, thereby helping developers analyze and optimize program performance.

Fourth, a static inspection scanning tool is an automated code review and review tool that helps developers find and resolve problems and errors in code early. By using static inspection scanning tools, developers can more thoroughly check their code for issues and errors, and improve code quality and readability.

Finally, human Code Review is a method of manually reviewing and reviewing code, which can examine problems and errors in code more deeply and provide better solutions and suggestions. Manual Code Review can promote team collaboration and learning, improve code quality and readability, and can also follow specific coding norms and standards.

In summary, unit testing, TDD, logging, static inspection scans, and manual code reviews are all necessary software development practices that improve code quality, readability, and maintainability, and facilitate team collaboration and learning. When doing software development, you should follow these practices as much as possible and use the appropriate tools and techniques for code review and testing.

Guess you like

Origin blog.csdn.net/wangonik_l/article/details/131641043