Unit testing of software testing technology-engineer Style's testing method

What is unit testing?

Wikipedia's definition of unit testing:

In computer programming, unit testing (Unit Testing), also known as module testing, is a test for the correctness of program modules (the smallest unit of software design).

In actual testing, a unit can be as small as a method or as large as containing multiple classes. By definition, there is a strict distinction between unit testing and integration testing, but in actual development they may not be so strict. If you specifically pursue unit testing, you must test the smallest unit, but it is easy to cause redundant tests and it is not easy to maintain. In other words, to put it more rigorously, we need to consider the test scenarios before choosing tests with different granularities.

Unit testing and integration testing can be performed manually or automatically. But now it is generally mentioned that unit testing refers to automatically executed tests. Therefore, the unit tests we mentioned below, unless otherwise specified, generally refer to automatically executed unit tests or integration tests.

Getting started with unit testing

Let's look at two cases first to get a feel for what unit testing looks like.

Example 1: Game of Life Unit Test

Let's first look at a very simple example, implementing a Conway's Game of Life. If you don't know Conway's Game of Life, you can read Wikipedia's introduction. Suppose we define an interface like this when implementing:

public interface Game {

void init(int[][] shape) ; // initialize the game board

void tick(); // progress to a tick

int[][] get(); // Get the current game board

}

The game of life has several rules. In order to test whether our implementation is correct, we can write a unit test for each rule of the game of life. The following test is the resurrection rule.

@Test

public void testRelive() {

int[][] shape = { {0, 0, 1}, {0, 0, 0}, {1, 0, 1}};

Game g = new GameImplSample(shape);

g.tick();

// I die, 3 living states around, revive

assertEquals(1, g.get()[1][1]);

}

Example 2: Order refund integration test

We are looking at a slightly more complex example, testing the process of refunding an order.

@Test

public void test300_doRefundItem() {

// create order, pay, then refund

Order order = createOrder(OrderSource.XR_DOCTOR);

order = fullPay(order, PayType.WECHAT_JS);

OrderItem item = _doItemRefund(order, 1, false);

// Check refunding status

OrderWhole orderWholeRefunding = findOrderWhole(order.getOrderNo());

isTrue(orderWholeRefunding.getRefundStatus().equals(

OrderRefundStatus.PARTIAL_REFUNDING));

isTrue(orderWholeRefunding.getRefunds().get(0).getStatus().equals(

RefundStatus.REFUNDING));

isTrue(orderWholeRefunding.getRefunds().get(0).getItemId().get().equals(

item.getId()));

// Build callback information for refund

List payments = findPayments(order.getId());

List refunds = findRefunds(order.getId());

wxRefundNotify(payments.get(0), refunds.get(0), WxRefundStatus.SUCCESS);

// check status after refund

OrderWhole orderWholeFinish = assertRefund(order, FULL_PAID,

PARTIAL_REFUND_OK, RefundStatus.SUCCESS, RefundMode.ITEM, false);

isTrue(orderWholeFinish.getRefundFee() == item.getPaidPrice());

isTrue(orderWholeFinish.getIncomes().stream()

.filter(i -> i.getAmount() < 0).count() == 1);

}

unit test execution

There are many ways to execute unit tests:

Execute in IDE

Run via mvn or gradle

Execute in CI

Either way, unit tests should be easy to run and give a test result. Of course, the unit test runs fast, usually at the second level. If it is too slow, it will not be able to get feedback in time.

Why write unit tests?

Benefits of unit testing

Make sure the code meets the requirements or design specifications. Using unit tests to test code, you can construct data and preconditions to ensure that the test covers the logic that needs to be tested. Manual testing or UI testing is not possible and tends to be more complex.

Quickly locate and solve problems. Because of the relatively small test scope, unit testing can locate the problem relatively easily; while manual testing often takes a lot of time to locate the problem.

Make sure the code always meets the requirements specification. Once the implementation needs to be modified, unit testing can ensure the correctness of the code and greatly reduce the risk of various modifications and refactorings. Especially avoid bugs that appear in unexpected places.

Simplify system integration. Unit testing ensures the correctness of the system or module itself, making it less prone to errors during integration.

Improve code quality and maintainability. Untestable code has some problems with its own abstraction, modularity, and maintainability. For example, it does not comply with design principles such as single responsibility and interface isolation, or relies on global variables. Testable code tends to be of relatively higher quality.

Provides documentation and instructions. Unit tests themselves are good examples of how interfaces are used.

Continuous Integration and Continuous Delivery

Around 2010, the system deployment of most Internet companies was still carried out manually, and the system often had to be launched in the middle of the night. However, the concept of continuous integration and continuous delivery has been continuously promoted, and the deployment process has become more and more flexible and smooth. Unit testing is an important part of continuous integration and continuous delivery.

Continuous integration is Continuous Integration (CI), which refers to the process of uploading code from development, automatic building and testing, and finally feedback results.

Furthermore, if after automatic building and testing, it will be automatically released to the test environment or pre-release environment, perform more tests (integration tests, automated UI tests, etc.), and even release directly at the end, then this process is continuous delivery (Continuous Delivery) , CD). Many companies in the industry, such as Amazon and Esty, can deploy dozens or even hundreds of production environments every day because they have a relatively complete continuous delivery environment.

CI is already an essential standard in the Internet industry, and CD has also been practiced more and more in the Internet industry, but without unit testing, the process of CI and CD is flawed.

How to write unit tests?

Introduction to JUnit

Basically every language and framework has good unit testing frameworks and tools, such as Java's JUnit, Scala's ScalaTest, Python's unittest, JavaScript's Jest, and so on. The above examples are based on JUnit, we will briefly introduce JUnit below.

Each @Test annotated method in JUnit is a test. @Ignore can ignore a test. @Before, @BeforeClass, @After, @AfterClass can insert some common operations before and after test execution, such as initialization and resource release, etc.

In addition to assertEquals, JUnit also supports many other assert methods. For example assertNull, assertArrayEquals, assertThrows, assertTimeout, etc. In addition, you can also use a third-party assert library such as Spring's Assert or AssertJ.

In addition to testing common code logic, JUnit can also perform exception testing and time testing. Exception test is to test that a certain piece of code must throw a specified exception, and time test is to test that the execution time of the code is within a certain range.

It is also possible to group tests. For example, it can be divided into contractTest, mockTest and unitTest, and the test of a certain group can be specified by parameters.

I won’t introduce too much here. If you want to know more about JUnit, you can go to the JUnit tutorial of Geek Academy and other materials. The basic functions of other unit testing frameworks are similar.

Use test Double

In a narrow unit test, we only test the unit itself. Even if we write a generalized unit test, it may still depend on other modules, such as methods of other classes, third-party service calls or database queries, etc., making it impossible for us to test the system or module under test very conveniently. Then we need to use the test Double.

If you study carefully, the test Double is divided into many types, such as what Dummies, Fakes and so on. But I think we only need to clarify two categories, that is, Stub and Mock.

Stub

Stubs are objects that contain predefined data and are returned to the caller during testing. Stub is often used in scenarios where we don't want to return real data or cause other side effects.

The Stub Jar generated by our contract test and that can be run through the spring cloud stubrunner is a Stub. We can let Stub return preset fake data, and then we can rely on these data in unit tests to test the code. For example, we can let the user query the Stub to return the authenticated user and the unauthenticated user according to the user ID in the parameter, and then we can test the caller's processing logic in these two cases.

Of course, the Stub may not be a remote service, but another class. So we often say that we need to program against the interface, because in this way we can easily create a Stub implementation of the interface to replace the specific class.

public class StubNameService implement NameService {

public String get(String userId) {

return ““Mock user name””;

}

}

public class UserServiceTest {

// UserService depends on NameService and will call its get method

@Inject

private UserService userService;

@Test

public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {

userService.setNameService(new StubNameService());

String testName = userService.getUserName(““SomeId””);

Assert.assertEquals(““Mock user name””, testName);

}

}

However, it is very troublesome to implement many stubs in this way. Now we don't need to create stubs by ourselves, because there are various mock tools.

Mock

Mocks refer to objects that can record their call information. In test assertions, we can verify that Mocks are called as expected.

The difference between Mock and Stub is that Stub only provides some data, it does not perform verification, or it only performs some verification based on state; Mock can also perform verification based on calling behavior in addition to doing what Stub does. For example, Mock can verify that the Mock interface is called exactly twice, and the parameters of the call are the expected values.

The most commonly used Mock tool in Java is Mockito. Let's look at a simple example, the following UserService depends on NameService. When we test UserService, we want to isolate NameService, so we can create a Mock NameService and inject it into UserService (in Spring, we only need to use @Mock and @InjectMocks two annotations to complete)

public class UserServiceTest {

@InjectMocks

private UserService userService;

@Mock

private NameService nameService;

@Test

public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {

Mockito.when(nameService.getUserName(““SomeId””)).thenReturn(““Mock user name””);

String testName = userService.getUserName(““SomeId””);

Assert.assertEquals(““Mock user name””, testName);

Mockito.verify(nameService).getUserName(““SomeId””);

}

}

Note that the last line above verifies that the getUserName of the nameService is called with the parameter ""SomeId"".

contract test

The contract test will generate a stub for each service, which can be used for unit/integration testing of the caller. For example, we need to test the reservation operation of the reservation service, and the reservation operation will call the user service to verify some basic information of the user, such as whether the doctor is certified or not.

Therefore, we can let the contract Stub return user data in different states by passing in different user IDs, thereby verifying different processing procedures. For example, a test case for a normal appointment flow might look like this.

@RunWith(SpringJUnit4ClassRunner.class)

@SpringBootTest@AutoConfigureStubRunner(repositoryRoot=““http://<nexus_root>””,

ids = {"“com.xingren.service:user-client-stubs:1.0.0:stubs:6565"”})public class BookingTest {

// BookingService will call the user service, and perform different processing after obtaining the doctor's authentication status

@Inject

private BookingService bookingService;

@Test

public void testBooking() {

BookingForm form = new BookingForm(

1,// doctorId

1 // scheduleId

1001);// patientId

BookVO res = bookingService.book(form);

assertTrue(res.id > 0);

assertTrue(res.payStatus == PayStatus.UN_PAY);

}

}

Note that the above AutoConfigureStubRunner annotation is to set and start the user service Stub. Of course, when testing, we need to set the baseUrl of the service call interface to http://localhost:6565. For more information about contract testing, please refer to the article Exploring integration testing in a microservice environment.

TDD

Simply put, Test Driven Development, or TDD. The left-eared mouse wrote an article that TDD is not as beautiful as it looks, so I directly quoted its introduction.

Its development process starts from the test case of functional requirements, first add a test case, then run all the test cases to see if there are any problems, then realize the functions to be tested by the test case, and then run the test case to see if any cases fail , then refactor the code, and repeat the above steps.

In fact, the strict TDD process is not very practical, and the left ear mouse itself is also critical. However, for modules with clear interface definitions, it is still beneficial to write unit tests first and then write implementation code. Because the goal is clear and you can get immediate feedback.

c

Article source: The network copyright belongs to the original author

The above content is not for commercial purposes, if it involves intellectual property issues, please contact the editor, we will deal with it immediately

Guess you like

Origin blog.csdn.net/xuezhangmen/article/details/132334245