×
Community Blog Testing Comes First to Ensure Code Quality in Complex Systems

Testing Comes First to Ensure Code Quality in Complex Systems

This article analyzes the problems faced by existing testing methods and shares some practical experiences using the GTest framework for unit testing.

By Qili

Test-Driven Development (TDD) is a development method that gives priority to tests. By writing unit testing cases, TDD can guarantee the quality of development and refactoring for existing complex systems. This article analyzes the problems faced by existing testing methods and shares some practical experiences using the GTest framework for unit testing.

1

1. Business Background

The online navigation service of AutoNavi (also known as AMAP) inevitably has a large amount of unreasonable code as an inventory system with strong business characteristics and years of historical accumulation. However, business evolution constantly puts forward higher requirements for system performance, algorithms, and underlying architecture. There are serious conflicts between the requirements for the rapid evolution of various business codes, algorithms, and architectures in stock. In this context, how to guarantee the quality of rapid refactoring evolution has become the primary engineering issue faced by business development.

2. Problems and Analysis of Existing Quality Assurance Methods

2.1 Problems of Existing Testing Methods

The conventional method is to use diff in batch requests to compare the new and old services. This method is simple and effective and has always been used. However, it has the following problems:

  • Invalid Diff: The bus planning engine relies on multiple downstream services, such as the step-by-step engine, search, bus emergencies, and road conditions. The difference in results obtained will result in many invalid diffs.
  • Long-Running Time: It takes a long time to run when there are a large number of cases at the level of 10 minutes. Due to the high cost of this step, the frequency of running the diff is not too high for developers, and they cannot perform the "one step at a time" test.
  • Troubleshooting Difficulty: When the diff is found, it is very difficult to check the problem because it is the diff of the entire request level, and there may be problems in the intermediate steps.

2.2 Industry Mainstream Method and Practice

Companies, such as ThoughtWorks and Google, use TDD to implement agile development and write unit testing cases to ensure the quality of development and refactoring. This has become a mainstream best practice.

3. Introduction to Unit Testing

3.1 What Is Unit Testing?

Unit testing is the test of a module, function, or class.

The test granularity is smaller and more lightweight, and the running time is at the second level. This makes it especially suitable for the "one step at a time" quality assurance in progressive refactoring.

Since a unit testing case is intended for a function or class with finer granularity, we can locate the problem when a use case is not applicable.

3.2 Unit Testing Framework

A common unit testing framework is the xUnit series, which is implemented in multiple languages, such as CppUnit, JUnit, and NUnit.

GTest is a unit testing framework developed by Google that provides advanced features, such as death and mock tests. We chose the GTest framework.

3.3 Unit Testing, Refactoring, TDD, and Agility

TDD is a development method that gives priority to tests. When you write any function or modify any code, you can write a unit testing case code to express the code function to be implemented. A test case is a requirement of code expression. The accumulated test cases can effectively ensure the quality of development and subsequent refactoring.

Refactoring and TDD are the core components of the agile approach. Agility without TDD is dangerous. Once refactoring without a use case guarantee is started, it is like a runaway horse. Unit testing and TDD are the reins of the horse.

4. Unit Testing Practice for Public Transport Service

4.1 GTest Framework Integration

Git Repository Address

The GTest framework integration is very simple. Just add the googletest library to the project, and add the link libgtest:

2

You can use the following code to drive the execution of use cases:

int RCUnitTest::Excute()
{
  int argc = 2;
  char* argv[] = {const_cast<char*>(""), const_cast<char*>("--gtest_output=\"xml:./testAll.xml\"")};
  ::testing::InitGoogleTest(&argc, argv);

  return RUN_ALL_TESTS();

Switch Control: You can consider compiling control or add a configuration item switch to avoid affecting the official version.

We use a configuration item at the entrance to control whether or not to trigger the unit testing case. During the compiling, only the entry file is linked by default. The unit testing case file needs to be added to the unit testing to run the link.

4.2 Test Code Writing

Implement a derived class of the test class, and then use the TEST_F macro to add a test function, as shown below:

class DateTimeUtilTest : public ::testing::Test
{
protected:
    virtual void SetUp()
{
    }

virtual void TearDown()
{
    }
};

TEST_F(DateTimeUtilTest, TestAddSeconds_leap)
{
    //Leap year test 2020-02-28
    tm tt;
    tt.tm_year = (2020 - 1900);
    tt.tm_mon = 1;
    tt.tm_mday = 28;
    tt.tm_hour = 23;
    tt.tm_min = 59;
    tt.tm_sec = 50;

    DateTimeUtil::AddSeconds(tt, 30);
    EXPECT_TRUE(tt.tm_sec == 20);
    EXPECT_TRUE(tt.tm_min == 0);
    EXPECT_TRUE(tt.tm_hour == 0);
    EXPECT_TRUE(tt.tm_mday == 29);
    EXPECT_TRUE(tt.tm_mon == 1);

    //Non-leap year test 2019-02-28
    tm tt1;
    tt1.tm_year = (2019 - 1900);
    tt1.tm_mon = 1;
    tt1.tm_mday = 28;
    tt1.tm_hour = 23;
    tt1.tm_min = 59;
    tt1.tm_sec = 50;
    DateTimeUtil::AddSeconds(tt1, 30);
    EXPECT_TRUE(tt1.tm_sec == 20);
    EXPECT_TRUE(tt1.tm_min == 0);
    EXPECT_TRUE(tt1.tm_hour == 0);
    EXPECT_TRUE(tt1.tm_mday == 1);
    EXPECT_TRUE(tt1.tm_mon == 2);
};

Execution Effect of Test Cases:

3

Currently, 23 module test cases have been accumulated for the bus engine, covering basic functions, such as station finding, road finding, Estimated Time of Arrival (ETA), fare, and risk outage. Through the unit testing guarantee, progressive refactoring activities are carried out in each version development activity, which can guarantee quality. The number of test iterations and the number of problems introduced by new online code are continuously low.

4

4.3 Problems and Difficulties

Data Dependency

The online navigation engine is a service that relies heavily on data. Multiple sets of data structures are interrelated and have a wide range of fields. Therefore, it is difficult to construct an effective unit testing without data. Using mock methods to construct false data is costly. Data changes will cause use cases to fail.

My experience: You can pass the use cases by creating fake data.

When it is difficult to create fake data, you can use real data. It does not matter if the use cases fail because of data changes. You only need to make sure all relevant use cases can be called before each refactoring to ensure the quality of the refactoring process. It is unnecessary to pass the use cases anytime and anywhere, but it is necessary to make sure they can be passed before and after refactoring.

4.4 Common Misconceptions

For those that have not implemented unit testing and TDD, some common misconceptions may occur:

If I do not have enough development time, how can I write unit testing?

My understanding:

  • First, the development method of TDD gives priority to tests, so you should write code first. This process is essential to understanding the requirements. In other words, you need to know the functions that you want to realize. This test code is the product of sorting out the requirements, so there is no more time cost.
  • TDD is a typical development method that benefits from one investment continuously. The more use cases there are, the easier it is to find problems in the early stage. With the quality assurance of refactoring, the code becomes cleaner and clearer, and developers no longer have to lament historical code.

Making up the Unit Testing with a Large Amount of Historical Code

Start by adding the first use case. My method is to add use cases corresponding to the code involved in this modification and accumulate use cases gradually.

The process of adding use cases is the process of understanding the existing code. For existing historical code, various hard-coded intrusions, various coupling, global variables, or long lifecycle large objects, you can effectively clarify the real input and output of functions and ensure effective refactoring by writing unit testing cases.

5. Progressive Refactoring of Existing Complex System Code

First-line programmers spend most of their time working with code every day. If the code you maintain is in a reasonable structure, easy to read, and scalable, you must feel delighted and satisfied. However, in most cases, programmers are faced with stock projects with various historical "accumulations," all of which are very important and can affect the whole project. In this case, you can make small changes by spending more time thinking carefully. However, it is difficult to perform some big system upgrades.

For mega-business systems, rewriting is unrealistic in terms of cost and quality control. After several large nodes are set and optimized gradually through progressive refactoring, the variables become qualitative, which is the optimal method based on the overall situation. Unit testing and TDD are necessary methods for effective refactoring.

0 0 0
Share on

FlyingFox

1 posts | 0 followers

You may also like

Comments

FlyingFox

1 posts | 0 followers

Related Products