About unit tests, how can I write testable code?

Source: Internet
Author: User

Unit testing is an essential and important part of a complete software development process. Writing unit tests is usually not difficult, but sometimes it's difficult to write tests because some code and features are hard to test. Therefore, it is very important to write good testable (testable) code. Next, let's briefly discuss what kind of code is hard to test, how we should avoid writing hard-to-test code, and some best practices for writing testable code.

What is unit test?

In computer programming, Unit testing ( English: Unittesting), also known as module testing , is a test for correctness testing of program modules (the smallest unit of software design). The program unit is the smallest testable part to be applied. In procedural programming, a unit is a single program, function, process, etc. for object-oriented programming, the smallest element is a method, including a base class (superclass), an abstract class, or a method in a derived class (subclass).

Typically, a unit test has three behaviors:

    1. Initializes the module or method that needs to be tested.
    2. Invokes a method.
    3. Observation results (assertions).

These three behaviors are called arrange, act and Assert, respectively. In Java, for example, the general test code is as follows:

@Test Public voidIspalindrome () {//Initialize: Initializes the module that needs to be tested, and here is an object. //There may also be no initialization module, such as testing a static method. Palindromedetector detector =NewPalindromedetector (); //Call Method: Records the return value for subsequent validation. //if the method has no return value, then we need to verify that it has an impact on other parts of the system during execution, or that it has side effects.         BooleanIspalindrome = Detector.ispalindrome ("Kayak"); //Assertion: Verifies that the returned result is the same as expected. assert.asserttrue (ispalindrome); }

Differences between unit tests and integration tests

The purpose of unit testing is to verify the behavior of the individual cells with the smallest granularity, such as a method, an object. With unit testing, we can ensure that each individual unit in the system is working properly. Unit tests are scoped only in this independent unit and do not depend on other units. The purpose of integration testing is to verify the function behavior of the whole system in real environment, and to combine the different modules to test. Integration testing usually requires that the project be started and may depend on external resources such as databases, networks, files, and so on.

The characteristics of good unit testing

1. Simple and Clear Code

We write multiple test cases for a single unit, so we want to overwrite all of the test cases with the simplest possible code.

2. Strong readability

The name of the test method should be straightforward to indicate the content and intent of the test, and if the test fails, we can quickly and easily locate the problem. With good unit testing, we can fix bugs without having to debug and break points.

3. Strong reliability

Unit tests are only really bugs in the unit under test and cannot rely on anything outside of the unit, such as global variables, the environment, the order in which configuration files or methods are executed, and so on. When these things change, the results of the test are not affected.

4. Fast execution speed

Usually we run unit tests every time we pack, and if it's very slow and inefficient, it can cause more people to skip tests locally.

5. Test only independent units

Unit testing and integration testing are different purposes, and unit tests should exclude the effects of external factors.

How to write code that can be tested

Let's start with a simple example to explore this problem. We are writing a smart home controller program, one of which is the need to turn on the lamp automatically at night when you touch the lamp. We use the following methods to determine the current time:

 Public StaticString gettimeofday () {Calendar Calendar=gregoriancalendar.getinstance (); Calendar.settime (NewDate ()); inthour =Calendar.get (Calendar.hour_of_day); if(Hour >= 0 && Hour < 6) {        return"Night"; }    if(Hour >= 6 && Hour < 12) {        return"Morning"; }    if(Hour >= && Hour < 18) {        return"Afternoon"; }    return"Evening";
}

What's wrong with the above code? If we look at the unit test, we will find that this code simply cannot write the test, the new date () represents the current time, this is an embedded in the method of the implicit input, the input is changing at any time, run this method at different times, The value returned will also be different. The unpredictability of this method leads to the inability to test. If you want to test, our test code might want to write this:

@Test  Public void gettimeofdaytest () {    try  {        //  Modify system time, set to 6 points ...          = gettimeofday ();        Assert.assertequals ("Morning", TimeOfDay);     finally {        //  Restore system time             ...    }}

Unit tests like this violate many of the characteristics of the good tests we have described above, such as the high cost of running the test (and the system time), which is unreliable (this test may fail due to a failure in setting the system time) and may be slower. Second, this approach violates several principles:

1. Methods and data sources are tightly coupled together

Time this input cannot be obtained from other data sources, such as obtaining time from a file or database.

2. Violation of the single principle of responsibility (Responsibility Principle)

SRP means that each class or method should have a single function. And this method has several responsibilities: 1. Gets the time from a data source. 2. Judge whether the time is morning or evening. An important feature of the SRP is that a class or a module should have and only one reason to change, in the above code, there are two reasons for the modification of the method: 1. The way to get the time changed (for example, to get time from the database). 2. The logic of the time is changed (for example, from 6 o'clock to 7 o'clock at the beginning of the night).

3. The responsibility of the method is not clear

Method signature String gettimeofday () The description of the method responsibility is not clear, the user does not enter the API to view the source code, it is difficult to understand the functionality of the API.

4. Difficult to predict and maintain

This method relies on a mutable global state (System time), and if the method contains multiple similar dependencies, then when you read this method, you need to look at the values of the environment variables it relies on, which makes it difficult to predict the behavior of the method.

Simple improvements
 Public StaticString Gettimeofday (Calendar time) {inthour =Time.get (Calendar.hour_of_day); if(Hour >= 0 && Hour < 6) {        return"Night"; }    if(Hour >= 6 && Hour < 12) {        return"Morning"; }    if(Hour >= && Hour < 18) {        return"Noon"; }    return"Evening";}

Now, this method does not have the responsibility to get time, his output depends entirely on the input passed. So it's easy to test it:

@Test  Public void gettimeofdaytest () {    = gregoriancalendar.getinstance ();     // set the time    Time.set (2018, 1, (), XX, xx);     = Gettimeofday (time);    Assert.assertequals ("Morning", TimeOfDay);}

Good ~ This method has testability, but the problem is still unresolved, now the responsibility to get time, to move to the higher level of the code, that is called the module of this method:

 Public classSmarthomecontroller {PrivateCalendar Lastmotiontime;

Public voidActuatelights (Booleanmotiondetected) { //update time of last touch if(motiondetected) {lastmotiontime.settime (NewDate ()); } //ouch!Calendar Nowtime =gregoriancalendar.getinstance (); Nowtime.settime (NewDate ()); //Judging TimeString TimeOfDay =Gettimeofday (nowtime); if(Motiondetected && ("Evening". Equals (TimeOfDay) | | "Night". Equals (TimeOfDay))) { //Touch the lamp at night and turn on the light! BackyardLightSwitcher.Instance.TurnOn (); } Else if(Getintervalminutes (Lastmotiontime, Nowtime) > 1 | | ("Morning". Equals (TimeOfDay) | | "Noon". Equals (TimeOfDay))) { //over a minute without touch, or during the day, turn off the lights! BackyardLightSwitcher.Instance.TurnOff (); } }}

To solve this problem, it is often possible to use dependency injection (inversion of control, IoC), and control inversion is an important design pattern that is especially effective for unit testing. In real-world engineering, most applications implement business logic by cooperating with each other, which allows each object to obtain a reference to the object it is working with (that is, the object that he depends on), which can cause the code to be highly coupled and difficult to test if it is implemented on its own. So how do you reverse it? That is to transfer control from the business object to the user, platform or framework.

Introduced the code after the inversion of control
 Public classSmarthomecontroller {PrivateCalendar Lastmotiontime; PrivateCalendar Nowtime;  PublicSmarthomecontroller (Calendar nowtime) {
This. nowtime = Nowtime; } Public voidActuatelights (Booleanmotiondetected) { //update time of last touch if(motiondetected) {lastmotiontime.settime (NewDate ()); } //Judging TimeString TimeOfDay =Gettimeofday (nowtime); if(Motiondetected && ("Evening". Equals (TimeOfDay) | | "Night". Equals (TimeOfDay))) { //Touch the lamp at night and turn on the light! BackyardLightSwitcher.Instance.TurnOn (); } Else if(Getintervalminutes (Lastmotiontime, Nowtime) > 1 | | ("Morning". Equals (TimeOfDay) | | "Noon". Equals (TimeOfDay))) { //over a minute without touch, or during the day, turn off the lights! BackyardLightSwitcher.Instance.TurnOff (); } }}

In the previous code, the acquisition of Nowtime was implemented by Smarthomecontroller itself, and after the introduction of control inversion, Nowtime was injected into the object by us when it was initialized. If you use the spring framework, the injected work is done by the spring framework, where control is transferred to the user or the framework, which is the meaning of control inversion.

Next, we can mock the time attribute in the test:

@Test  Public void testactuatelights () {    = gregoriancalendar.getinstance ();    Time.set (2018, 1, (), XX, xx);     New Smarthomecontroller (time);    Controller.actuatelights (true);    Assert.assertequals (Time, Controller.getlastmotiontime ());}

Here, it is easy to do unit testing, you think this code has a good testability?

Side effects of the method (Side effects)

Let's take a closer look at the code to turn off the lights:

 if  (motiondetected && ("Evening". Equals (TimeOfDay) | | "Night" .equals (TimeOfDay)) { //  evening touch the lamp, turn on the light!   BackyardLightSwitcher.Instance.TurnOn ();}  else  if  (Getintervalminutes (Lastmotiontime, Nowtime) > 1 | |  "Morning". Equals (TimeOfDay) | | "Noon" .equals (TimeOfDay)) { //  over a minute without touch, or during the day, turn off the lights!   BackyardLightSwitcher.Instance.TurnOff ();}  

This control of the table lamp by controlling the Backyardlightswitcher , which is a global variable, means that each time the unit test is run, the value of the variable in the system may be modified. In other words, this test has a side effect. If other unit tests also depend on the value of the backyardlightswitcher , the result of the test becomes uncontrolled. Therefore, this method still does not have the good testability.

Functional, class-a citizen

Java8 introduced the concept of functional and class-I citizens. The object that we are familiar with is the abstraction of the data, and the function is the abstraction of some kind of behavior.

The First class function (first-class functions) refers to the programming language in which functions are treated as first-class citizens. This means that a function can be assigned to a variable or stored in a data structure as an argument to another function, as a function's return value. [1] Some advocates should include support for anonymous functions (the number of letters, function literals). [2] in such a language, the names of functions do not have a special meaning, they are treated as ordinary variables with a function type.

In fact, we can see that the above function is still not in line with the principle of single responsibility, it has two responsibilities: 1. Determines the current time. 2. Operate the lamp. We will now remove the duty to operate the lamp from this method and pass it in as a parameter:

@FunctionalInterface  Public Interface Action {    void  doAction ();}
 Public classSmarthomecontroller {PrivateCalendar Lastmotiontime; PrivateCalendar Nowtime;  PublicSmarthomecontroller (Calendar nowtime) { This. Nowtime =Nowtime; }     Public voidActuatelights (Booleanmotiondetected, Action turnOn, action turnoff) {        //update time of last touch        if(motiondetected) {lastmotiontime.settime (NewDate ()); }        //Judging TimeString TimeOfDay =Gettimeofday (nowtime); if(Motiondetected && ("Evening". Equals (TimeOfDay) | | "Night". Equals (TimeOfDay))) {            //Touch the lamp at night and turn on the light! turnon.doaction (); } Else if(Getintervalminutes (Lastmotiontime, Nowtime) > 1 | |            ("Morning". Equals (TimeOfDay) | | "Noon". Equals (TimeOfDay))) {            //over a minute without touch, or during the day, turn off the lights! turnoff.doaction (); }    }}

Now, to test this method, we can pass the virtual behavior in:

@Test Public voidtestactuatelights () {Calendar time=gregoriancalendar.getinstance (); Time.set (2018, 10, 1, 06, 00, 00); Mocklight Mocklight=Newmocklight (); Smarthomecontroller Controller=NewSmarthomecontroller (time); Controller.actuatelights (true, Mocklight::turnon, Mocklight::turnoff); Assert.asserttrue (Mocklight.turnedon);}//for testing Public classMocklight {BooleanTurnedon; voidturnOn () {Turnedon=true; }    voidturnoff () {Turnedon=false; }}

Now, we really have a testable approach that is very stable and reliable, without worrying about side effects on the system, and we have an easy-to-understand, readable, reusable API.

In functional programming, there is a concept called pure function, the main characteristic of pure function is:

  • This function will produce the same output when the same input value is entered. The output of the function is independent of the hidden information or state other than the input value , and is independent of the external output generated by the I/O device.
  • The function cannot have semantically observable function side effects, such as "triggering events", outputting the output device, or changing the contents of an object other than the output value.

Functions like this are generally very good testability, it is easy to do unit testing, and do not have problems, we need to do is to pass the parameters in, and then check the return results. For impure functions, such as a function Foo () , which relies on a function Bar () with side effects, foo () also becomes a function with side effects, and eventually, side effects can spread throughout the system.

Reference: Https://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters

About unit tests, how can I write testable code?

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.