This is the third day of my participation in Gwen Challenge


It is necessary for experienced developers to write unit tests and improve their code quality and coding ability. Unit testing can help reduce bug leaks. By running unit tests, the correctness of each function can be directly tested. Bugs can be found and solved in advance. At the same time, sufficient UT is an effective means to ensure the correctness of reconstruction. Only with sufficient UT protection can existing code be reconstructed freely and boldly. After years of work, I have a better understanding of UT and the importance of UT.

Unit testing

In the agile development philosophy, full coverage of automated testing is a necessary prerequisite for adding new features and refactoring. The importance of unit testing in software development process is self-evident, especially in the test-driven development mode more and more popular premise, unit testing has become an indispensable part of software development process. At the same time, unit testing is also an important method to improve software quality and lower cost.

1. Unit test timing and test points

1.1 Timing of unit testing

  1. Writing unit tests in front of business code is test-driven development, which we often use and recommend.
  2. Unit tests are conducted during the business code process, adding tests to important and complex business logic.
  3. It is not recommended to write tests after the business logic, unless the modification of legacy code requires the addition of test cases first to ensure that the modified and refactored code does not break the previous business logic.

1.2 Test points for unit tests

  1. Add tests to logically complex code.
  2. Add tests where errors are likely.
  3. Add tests to hard-to-understand code, and when you look at the tests later, it becomes clear what logic the code is trying to implement.
  4. Add tests to code that allows for relatively large changes in requirements later, so you don’t have to worry too much about writing the right code and breaking the logic of the existing code.
  5. Add decoupled code and unit tests to external interfaces.

2. Root causes of untestability

  1. The code calls interfaces of the underlying platform or resources that are available only after the system runs (database connections, mail sending, network communications, remote services, file systems, etc.), but the business code is not decouple from these resources. This will cause these resources to be initialized when the test code needs to create the class, making it impossible to test.
  2. New an object inside the method that is not relevant to this test.
  3. The code dependency level is deep, the logic is complex, a method often has to call the bottom interface N times, or the method of the class is very many. In such code, we need to refactor the class so that it has as single a responsibility as possible: the purpose of the class in the system should be single, and there should be only one reason to change it.
  4. Use singleton classes and static methods that use our underlying interface or some other interface.

3. Test tool usage and test method introduction

When we do unit testing, we find that the methods we test refer to many external dependencies, such as calling platform interfaces, connecting to databases, network communications, remote services, FTP, file systems, and so on. We have no control over these external dependent objects. To solve this problem, we need to use Mock tools to simulate these external dependent objects for unit testing. Some of the most popular Mock tools are JMock, EasyMock, Mockito, and PowerMock. We used Mockito and PowerMock. PowerMock makes up for the fact that the other three Mock tools cannot Mock static, final, or private methods. Mock objects can be used for unit testing in the following cases.

  1. Real objects have indeterminate behavior and produce unpredictable results. For example, a database query can find one record, multiple records, or return results such as database exceptions.
  2. Real objects are hard to create. For example: platform code, or Web, JBoss container, etc.
  3. Some behaviors of real objects are hard to trigger. For example, network exceptions, database exceptions, and message sending exceptions need to be handled in the code.
  4. Reality makes the program run slowly. In agile practice we have done CI, the unit test case of the entire project needs to be executed before the development and submission of the code, and the code can only be submitted after the test passes. This requires us to keep each unit test case as short as possible so that the test time of the whole project is short. Mock objects are used when test cases need to test the system’s expectations for large amounts of data.

For example, we need to judge in our code that only when the cache queue of the system is greater than 40,000, we start to consider discarking non-critical messages; when the cache queue exceeds 48,000, we need to process only the most important messages; when the cache queue exceeds 50,000, we need to discard all messages. At this point you need to Mock the cache queue, returning different amounts of data to the test depending on the call. 5. Tests need to know how real objects are called. For example, if a test case needs to verify that JMS is sent, it can be tested by whether a Mock object is invoked. 6. When the real object does not actually exist. For example, when we interact with other modules, or when we are dealing with new interfaces, or even when their code is not fully developed, we can use mocks to simulate the interface behavior and verify and test the code logic.

3.1 Simple instructions for Mocktio

Mocks can simulate a wide variety of objects to make the desired response in place of real objects.

1. Create simulated objects

List cache = mock(ArrayList.class);
System.out.println(cache.get(0));
//-> null Returns null because there is no expectation for mock objects
Copy the code

2. Mock the return value of the object method call

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("hello");
System.out.println(cache.get(0));
//-> hello
Copy the code

3. Mock object methods for multiple calls and multiple returns

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("0").thenReturn("1").thenReturn("2");
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
//-> 0,1,2,2 if the actual number of calls exceeds the expected number, the expected value of the last call is always returned.
Copy the code

4. Mock object method calls throw exceptions

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn(new Exception("Exception"));
System.out.println(cache.get(0));
Copy the code

5. Mock object methods can throw exceptions even when they return no value

List cache = mock(ArrayList.class);
doThrow(new Exception("Exception")).when(cache).clear();
Copy the code

6. Simulate parameter matching during method calls

The use of AnyInt matches anyintList cache = mock(arrayList.class); when(cache.get(anyInt())).thenReturn("0");
System.out.println(cache.get(0));
System.out.println(cache.get(2));
/ / - > 0, 0
Copy the code

7, simulate whether the method is called and the number of times called, expected to call once

List cache = mock(ArrayList.class);
cache.add("steven");
verify(cache).add("steven");
Copy the code

Expected calls to the cache twice, no calls to clear the cache

List cache = mock(ArrayList.class);
cache.add("steven");
cache.add("steven");
verify(cache,times(2)).add("steven");
verify(cache,never()).clear();
Copy the code

You can also use atLeast(int I) and atMost(int I) instead of times(int I) to verify the minimum and maximum number of calls. By default, Mocktio returns a default value for any method that returns a value that is not expected. Defaults are returned for built-in types, such as int, 0, and Boolean, false. Null is returned for other types. A mock object overwrites the entire object being mocked, so no expected method returns the default value. This is important when using a Mock for the first time. You will often find that your test results are wrong, only to discover that you did not give the appropriate expectations.

3.2 How to Use PowerMock

PowerMock uses a custom classloader and bytecode operations to simulate static methods, constructors, final classes and methods, private methods, removal of static initializers, and more. PowerMock is simple to use, adding annotations before the class name and calling PowerMock’s mock static class methods before expectations. Other expected methods are similar to Mockito.

@PrepareForTest(System.class)
@RunWith(PowerMockRunner.class)
public class Test {
@org.junit.Test
public void should_get_filed(a) {
    System.out.println(System.getProperty("myName"));
    PowerMockito.mockStatic(System.class);
    PowerMockito.when(System.getProperty("myName")).thenReturn("steven");
    System.out.println(System.getProperty("myName"));
    //->null steven}}Copy the code

3.3 Use of Fake Objects

Mock objects are required for testing, and we often use Fake objects in addition to the usual mock objects. Mock objects are pre-planned objects with expectations that form a detailed description of the calls they expect to receive. Fake objects have real working implementations, but often have some drawbacks that make them unsuitable for production, and we often use Fake objects to simulate real objects in our tests. In the test, we often find that we need to use the interface provided by the system or platform. In the test, we can create a new class to realize this interface, and then practice the corresponding method of this simulation class according to the specific situation.

If we create our own FakeLog object to simulate real Log printing, we can use FakeLog in the test class instead of the real Log class in the code. We can compare FakeLog methods with the expected results to determine the correctness of the test.

Another difference between Fake objects and mock objects is that once a Fake object is constructed, all future code should call the Fake object instead of expecting it every time. From this perspective, you can use Fake objects when the methods or expectations of a class are relatively unchanged, or MOCK objects when the expected changes in the return information of the class are very unexpected.

3.4 Two methods of Mock Services

(1) Direct injection: it is used for testing the whole business process with large granularity when there are many dependency levels between classes.

Service service = mock(Service.class);
new Processor().process(service );
Copy the code

(2) Overwrite protected methods to return mock objects: used in cases where classes directly depend on the service to test the details of behavior in a small granularity.

Service service = mock(Service .class);
generator = new Generator() {
    @Override
    protected Service getService(a) {
        returnservice; }}Copy the code

3.5 Test Exception

Throwable has two direct subclasses: Exception and Error

1, expcetd = SomeExecption. Class

@Test(expected = AssertionError.class)
public void should_occur_assertion_error_when_xx(a) throws Exception {
    new Processor().process();
}

@Test(expected = NumberFormatException.class)
public void should_throw_number_format_exception_when_xx(a) {
    Convert.convert2Long();
}
Copy the code

2. Try-catch-fail can only be used for exceptions, not errors

try {
    method.invoke();
    fail();
} catch (Exception e) {
    assertTrue(e.getCause() instanceof RuntimeException);
}
Copy the code

3.6 Private Methods – Use reflection to call

@Test
public void should_throw_runtime_exception_when_check_data_fail(a) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
   
    Method method = Generator.class.getDeclaredMethod("check", Item.class);
    method.setAccessible(true);
    try {
        method.invoke(Generator, mock(Item.class));
    } catch (Exception e) {
        assertTrue(e.getCause() instanceofRuntimeException); }}Copy the code

4. Format of unit tests

4.1 Test class structure

public class ExampleTest {
    @BeforeClass
    public static void setUp(a) throws Exception {
        initGlobalParameter();
        registerServices();
    }

    @Before
    public void setUp(a) throws Exception {
        initGlobalParameter();
        registerServices();
    }

    @After
    public void tearDown(a) throws Exception {
        ServiceAccess.serviceMap.clear();
        clearCache();     }

    @AfterClass
    public static void tearDown(a) throws Exception {
        ServiceAccess.serviceMap.clear();
        clearCache();
    }

    @Test
    public void should_get_some_result1_when_give_some_condition1{
    }

    @Test
    public void should_get_some_result2_when_give_some_condition2{
    }
}
Copy the code

JUnit4 is the biggest improvement to the JUnit framework ever, and its main goal is to simplify test case writing by taking advantage of Java5’s Annotation feature. So a quick explanation of what an Annotation is, the word Annotation is usually translated as metadata. What is metadata? Metadata is data that describes data. That is, this object can be used in Java to modify class, method, or variable names in the same way as the public or static keyword. The modifier describes what the data is used for, in much the same way that public describes what the data is public.

  • @before: Each test method should be executed once Before it is executed.
  • @after: before corresponds to each test method executed once.
  • @beforeClass: Run before all test methods, only once. It is common to apply for expensive external resources in this category. There is the @beforeClass method in the parent class, which also runs before its subclasses.
  • AfterClass: corresponds to BeforeClass. After all tests are complete, the resources applied for in the BeforeClass are released. Note: @before, @after, @beforeClass, @afterClass can only have one method in a class
  • @test: Tells JUnit to run the method as a Test case.

4.2 Location of the test code

In Java, a package can span two different directories, so our test code and production code are in the same directory, which is easier to maintain, the test code and production code are in the same package, which also reduces unnecessary package generation, and it is easier to use inheritance in the test class.

4.3 Test case format: three sections

The body content of a test case is usually in three paragraphs: given-when-then

  • Given: construct test conditions;

  • When: Executes the method to be tested;

  • Then: Determines whether the test results meet expectations.

Such as:

@Test
public void should_get_correct_result_when_add_two_numbers(a) {
    int a = 1;
    int b = 2;

    int c = MyMath.add(a, b);

    assertEquals(3, c);
}
Copy the code

4.4 Naming Method of class Names

The name of the Test class ends with Test. Derived from the class name of the target class is the class name of its unit test class. Add the suffix Test to the class name. Fake (pseudo class) is placed in the test package, using the prefix Fake.

4.5 Method Name Definition

Should...doSomething... The when... Under some the conditions...Copy the code

Such as:

should_NOT_delete_A_when_exists_B_related_with_A
should_throw_exception_when_the_parameter_is_illegal
Copy the code

4.6 Annotations of methods provided for testing in the business code

Protection methods or other methods that are provided separately in business code for testing purposes are annotated by @fortest. The FofTest class is as follows:

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface ForTest {
    String description(a) default "";
}
Copy the code

5. How to write unit tests when external interfaces are involved in your code

Our code involves a large number of modules and often needs to work together to complete a function, often using external interfaces and providing services to other modules in the process.

5.1 database

The unit test of the database, because the test can not connect the database, so we extract the general interface (DBManagerInterface) and FakeDBManager to achieve database decoupling. FakeDBManager can simulate a real database, which means we Fake a simple in-memory database to simulate a real database. DBManager is our business class that actually connects to the database. In our tests, it was possible to replace DBManager with FakeDBManager by injection.

5.2 Platform Interfaces

5.2.1 Mock platform interface

Any service interface in the platform can be mock tested. Note that the corresponding decoupling is required in the business code, which can be injected into the platform’s service classes through a SET method or constructor.

public class ListenerTest {
    private ServerService  service = mock(ServerService.class);

@Before
public void setUp(a) throws Exception {
    when(service.getIp()).thenReturn("127.0.0.1");
    when(service.getPort()).thenReturn("80");
    when(service.getTcpPort()).thenReturn("8080");
}
Copy the code

Note that if static variables are globally unique, they need to be cleared in tearDown after use.

5.3 Testing file interfaces

Our business also has code that reads and writes to external files. According to the principle of unit test writing, unit tests should be independent and not dependent on any external files or resources. Good unit tests run fast and help us locate problems. So any code that involves an external file, such as a mock (I18n) file or data in a properties or XML file, needs to anticipate information from a mock. For some important files, considering the low resource consumption, we will also add unit tests for these files. To access the actual file, the first step is to get the location of the resource file. Get the root directory of the unit test run from FileService’s getFileWorkDirectory.

public class FileService {
public static String getFileWorkDirectory(a) {
    return new StringBuilder(getFileCodeRootDirectory()).append("test").toString();
}

public static String getFileCodeRootDirectory(a) {
    String userDir = System.getProperty("user.dir");
    userDir = userDir.substring(0, userDir.indexOf(File.separator + "CODE" + File.separator));
    StringBuilder workFilePath = new StringBuilder(userDir);
    workFilePath.append(File.separator).append("CODE").append(File.separator);
    returnworkFilePath.toString(); }}Copy the code

We can access the real file in the test code by passing in a specific file name in the unit test. This approach can be applied to I18n files, XML files, and properties files. When we test I18n files, we can also use Fake objects to test the internationalization information according to the specific language. The code for FakeI18nWrapper is provided in Chapter 7.

@Before
public void setUp(a) throws Exception {
    String i18nFilePath = FileService.getFileWorkDirectory() + "\\conf\\i18n.xml";
    I18N i18N = new FakeI18nWrapper(new File(i18nFilePath), I18nLanguageType.en_US);
    I18nOsf.setTestingI18NInstance(i18N);
}
Copy the code

6. Unit test involves multithreading, singleton class, static class processing

6.1 Multithreading Test

With unit testing, bugs can be found earlier and fixed more easily than without unit testing. But normal unit testing methods, even when thoroughly tested, are not very effective at finding parallel bugs. That’s why there are no problems in lab tests, but all sorts of puzzling problems in the outfield. Why do unit tests often miss parallel bugs? It is often said that the problem with parallel programs and bugs is that they are uncertain. But for unit testing purposes, the parallel program is pretty much guaranteed. So our unit tests need to be multithreaded for critical logic, scenarios involving concurrency. The uncertainty of multithreading and the determined expectations of unit testing are indeed somewhat contradictory, which requires careful design of multithreaded use cases in unit testing. Junit itself does not support normal multithreaded testing because the underlying implementation of Junit is performed with the System.exit use case. The JVM is dead, and any other threads started in the test thread cannot execute either. So to write a multithreaded Junit test case, you have to have the main thread wait for all the child threads to finish executing before exiting. Our general approach is to add sleep to the main test thread. This approach has the advantage of simplicity, but the disadvantage is that the configuration of different machines is different, resulting in uncertain wait time. More efficient multithreaded unit tests can be implemented using JAVA’s CountDownLatch and third-party component GroboUtils. Here is a simple example to illustrate multithreaded unit testing. The business code for the test is as follows, and the function is a generator of unique transaction numbers.

class UniqueNoGenerator {
    private static int generateCount = 0;

    public static synchronized int getUniqueSerialNo(a) {
        returngenerateCount++; }}Copy the code

6.1.1 Sleep

private static Set results = new HashSet<>();

@Test
public void should_get_unique_no(a) throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i = 0; i < threads.length; i++) {
    threads[i] = generateThread();
    }
    // Start the thread
    Arrays.stream(threads).forEach(Thread::start);
    Thread.sleep(100L);
    
    assertEquals(results.size(), 100);
 }

private Thread generateThread(a) {
    return new Thread(() -> {
        int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
        results.add(uniqueSerialNo);
    });
}
Copy the code

Sleep is used to wait for all threads in the test thread to complete before anticipating the condition. The problem is that the user cannot accurately predict how long the business code thread will take to execute, and the waiting time varies from environment to environment. Because of the added latency, it also violates our principle of keeping unit test execution time as short as possible.

6.1.2 ThreadGroup

private static Set<Integer> results = new HashSet<>();
private ThreadGroup threadGroup = new ThreadGroup("test");

@Test
public void should_get_unique_no(a) throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i = 0; i < threads.length; i++) {
    threads[i] = generateThread();
 }
    // Start the thread
    Arrays.stream(threads).forEach(Thread::start);
    while(threadGroup.activeCount() ! =0) {
    Thread.sleep(1);
    }
    assertEquals(results.size(), 100);
    }
    
    private Thread generateThread(a) {
    return new Thread(threadGroup, () -> {
    int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
    results.add(uniqueSerialNo);
    });
}
Copy the code

This is implemented with a ThreadGroup. You can put the classes that need to be tested into a ThreadGroup and determine whether there are any unfinished threads in the ThreadGroup. Add the new thread to the thread group.

Also 6.1.3 CountDownLatch

private static Set<Integer> results = new HashSet<>();
private CountDownLatch countDownLatch = new CountDownLatch(100);

@Test
public void should_get_unique_no(a) throws InterruptedException {
    Thread[] threads = new Thread[100];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = generateThread();
    }
    // Start the thread
    Arrays.stream(threads).forEach(Thread::start);
    countDownLatch.await();

    assertEquals(results.size(), 100);
}

private Thread generateThread(a) {
    return new Thread(() -> {
        int uniqueSerialNo = UniqueNoGenerator.getUniqueSerialNo();
        results.add(uniqueSerialNo);
        countDownLatch.countDown();
    });
}
Copy the code

JAVA’s CountDownLatch makes it easy to determine if the thread under test has completed execution. CountDownLatch is a synchronization helper class that allows one or more threads to wait until they complete a set of operations that are being performed on other threads, in our case the test main thread. The countDown method is called by the current thread and the count is reduced by one. Awaint method, which blocks the current thread until the value of the timer is 0.

6.2 Singleton Class Tests

Key points of singleton pattern:

  1. A singleton class has only one instance in a container.
  2. A singleton class provides its own instance to the client using static methods and has its own reference.
  3. You must provide your own instance to the entire container.

There are many ways to implement the singleton class, such as lazy singleton, hungry singleton, registration singleton, etc. Here we use the form of inner class to construct the singleton class, the advantage of implementation is that this method does not need to add locks to the class or method, the generation of the unique instance is guaranteed by JAVA inner class generation mechanism. The following example constructs a singleton class that provides a method to get remote Cpu information. Construct another class using Resourcemanager.java to simulate calling this singleton class, and look at the problems we encountered in testing Resourcemanager.java. The singleton class dbManagerTools.java:

public class DbManager {
         private DbManager(a) {}public static DbManager getInstance(a) {
         return DbManagerHolder.instance;
         }
         
         private static class DbManagerHolder {
         private static DbManager instance = new DbManager();
         }
         
         public String getRemoteCpuInfo(a){
         FtpClient ftpClient = new FtpClient("127.0.0.1"."22");
         returnftpClient.getCpuInfo(); }}Copy the code

Call the class ResourceManager. Java:

public class ResourceManager {
    public String getBaseInfo(a) {
        StringBuilder buffer = new StringBuilder();
        buffer.append("IP=").append("127.0.0.1").append("; CPU=").append(DbManager.getInstance().getRemoteCpuInfo());
        returnbuffer.toString(); }} Test class@Test
public void should_get_cpu_info(a) {
    String expected = "IP = 127.0.0.1; CPU=Intel";
    ResourceManager resourceManager = new ResourceManager();

    String baseInfo = resourceManager.getBaseInfo();

    assertThat(baseInfo, is(expected));
}
Copy the code

As can be seen from the above description, because the business code is strongly associated with a singleton class, and this singleton class will fetch information about the remote machine over the network. Thus, our unit test will fail to connect to a server in the network while it is running. Similar calls involving singleton classes are often used in business classes. In this case we need to modify the business code to make it testable. The first method: extract the method and override it in the test class.

public class ResourceManager {
    public String getBaseInfo(a) {
        StringBuilder buffer = new StringBuilder();
        buffer.append("IP=").append("127.0.0.1").append("CPU=").append(getRemoteCpuInfo());
        return buffer.toString();
    }

    @ForTest
    protected String getRemoteCpuInfo(a) {
        returnDbManager.getInstance().getRemoteCpuInfo(); }}@Test
public void should_get_cpu_info(a) {
    String expected = "IP = 127.0.0.1; CPU=Intel";
    ResourceManager resourceManager = new ResourceManager(){
        @Override
        protected String getRemoteCpuInfo(a) {
            return "Intel"; }}; String baseInfo = resourceManager.getBaseInfo(); assertThat(baseInfo, is(expected)); }Copy the code

The second approach is to extract the methods in the singleton class as interfaces and inject them into the business code through the set method or constructor.

public class DbManager implements ResourceService{
    private DbManager(a) {}public static DbManager getInstance(a) {
        return DbManagerHolder.instance;
    }

    private static class DbManagerHolder {
        private static DbManager instance = new DbManager();
    }

    @Override
    public String getRemoteCpuInfo(a){
        FtpClient ftpClient = new FtpClient("127.0.0.1"."22");
        return ftpClient.getCpuInfo();
    }

public interface ResourceService {
    String getRemoteCpuInfo(a);
}

public class ResourceManager {
    private ResourceService resourceService = DbManager.getInstance();

    public String getBaseInfo(a) {
        StringBuilder buffer = new StringBuilder();
        buffer.append("IP=").append("127.0.0.1").append("CPU=").append(resourceService.getRemoteCpuInfo());
        return buffer.toString();
    }

    public void setResourceService(ResourceService resourceService) {
        this.resourceService = resourceService; }}@Test
public void should_get_cpu_info(a) {
    String expected = "IP = 127.0.0.1; CPU=Intel";
    ResourceManager resourceManager = new ResourceManager();
    DbManager mockDbManager = mock(DbManager.class);
    resourceManager.setResourceService(mockDbManager);
    when(mockDbManager.getRemoteCpuInfo()).thenReturn("Intel");
    
    String baseInfo = resourceManager.getBaseInfo();

    assertThat(baseInfo, is(expected));
}
Copy the code

The above method can be used to ease the heavy dependence of business code on singletons. Sometimes we find that our business code is static. In this case, you may find that the first method does not solve the problem and can only be implemented through the second method. From the above code, we can see that we should use singletons as little as possible. When we must use singletons, we can design interfaces to decouple the business from the singleton class.

6.3 Static Class Testing

Static classes, like singleton classes, can be decoupled by extracting methods and then replaying them, as well as by service injection. PowerMock can also be used to anticipate the return of a method. In practice, if a singleton class does not need to maintain any state and only provides globally accessible methods, then static classes can be used. Static methods are faster than singletons because static binding is done at compile time. It is also important to note that maintaining state information in static classes is not recommended, especially in concurrent environments where modifying multithreaded concurrency without proper synchronization measures can lead to bad race conditions. The main advantage of singletons versus statics is that singletons are more object-oriented than static classes. Using singletons, you can extend base classes through inheritance and polymorphism, implement interfaces, and have the ability to provide different implementations. With unit testing in mind during our development process, we still need to use static and singleton classes with caution.

7. Decoupling methods for code testability

When using some of the de-dependency techniques, we often feel that many of them break the original encapsulation. But considering the testability and quality of the code, it’s okay to sacrifice some encapsulation, and encapsulation is not an end in itself, but rather an aid to understanding the code. The following describes the common methods of decompenting dependencies. The idea of these de-dependency methods is general, and they are carried out by inversion of control and dependency injection.

7.1 Minimize coupling between business code and platform code

Typical code for calling platform services to query resource attributes in software development:

public class DataProceeor{
    private static final SomePlatFormService service = ServerService.lookup(SomePlatFormService.ID);
    public static CompensateData getAttributes(String name){ service.queryCompensate(name); }}Copy the code

This code is implementationally sound, but cannot be unit tested (without starting the software). There is a strong coupling between the business code and the platform code because the services related to platform query resources need to be acquired during this type of loading. On the basis of not breaking the original function of the code to do the following modification:

Introduce instance variables and constructors

public class DataProceeor{
    private static final SomePlatformService service = ServerService.lookup(SomePlatformService.ID);
    private SomePlatformService _service;

    public DataProceeor(SomePlatformService service) {
        _service = service;
    }

    public DataProceeor(a) {
        _service = ServerService.lookup(SomePlatformService.ID);;
    }

    public CompensateData getAttributes(String name){ service.queryCompensate(name); }}Copy the code

2. Add new methods

public CompensateData getSomeAttributes(String name){
    _service.queryCompensate(name);
}
Copy the code

Find all places in the code where the getAttributes method is used and replace them with getSomeAttributes.

4. After step 3, delete variables and methods that are no longer useful.

5. Rename imported variables and methods to conform to naming conventions.

public class DataProceeor{
    private SomePlatformService service;
    public DataProceeor(SomePlatformService service){
        this.service = service;
    }

    public DataProceeor(a) {
        service = ServerService.lookup(SomePlatformService.ID);;
    }

    public static CompensateData getAttributes(String name){ service.queryCompensate(name); }}Copy the code

6. Add test cases for the new method

public class DataProcessorTest {
    private DataProceeor dataProceeor;
    private SomePlateService somePlateService;
    private Map<String, String> attributes;

    @Before
    public void setUp(a) throws Exception {
        attributes.put("value"."1");
    }

    @Test
    public void should_get_attributes(a) {
        somePlateService = mock(SomePlateService.class);
        when(somePlateService.queryAttribue()).thenReturn(attributes);

        dataProceeor = new DataProceeor();

        Data data = dataProceeor.getAttributes("value");
        assertThat(data.value(), is("1"));
        assertThat(data.value(), is("2")); }}Copy the code

AssertThat (data.value(), not(“2”)); Run the test again and the test case passes.

7.2 Extend some classes of the platform to achieve the purpose of testing

The example in pattern 1 queries resource properties with no filter criteria set, and in fact most of the processing relies on other processing classes:

public class ClassA {

    public void processA (a) {
            ClassBProcessor processor new ClassBProcessor(a);
            processor.processB();
        } catch(Exception e) { logger.warn(e); }}}Copy the code

In this case, the Filter for the processB method is constructed inside the processA method. We can try to write a test case for the processA method: The test case didn’t pass. What’s the problem? The Debug code finds that the Filter constructed inside the processA method is not the same object as the Filter constructed in our test code. It’s a natural thought to subclass the Filter class and override its equals method. Replace the platform ClassBProcessor with a custom ClassBProcessor:

public String getClassBProcessor(a){
    ClassBProcessor filter = new SelfClassBProcessor();
    return filter);
}
Copy the code

The modified test case runs successfully.

7.3 Skillfully use ProtedTED method to achieve test injection

In pattern 2, because the ClassBProcessor is constructed within processA and there is no Euqals method, it cannot be tested. You can modify it in other ways. 1. Extract the protected method buildProcessor()

public class ClassA {

    public void processA (a) {
            ClassBProcessor processor new ClassBProcessor(a);
            processor.processB();
        } catch(Exception e) { logger.warn(e); }}}@ForTest
protected ClassBProcessor buildProcessor(a) {
        return new ClassBProcessor();
}
Copy the code

2. Override the buildProcessor method in the test code

@Before
public void setUp(a) throws Exception {

    private ClassBProcessor classBProcessor;
    ClassA classA = new ClassA(){
        @Override
       protected ClassBProcessor buildProcessor(a) {
            returnclassBProcessor; }}; }Copy the code

Run the test and it passes.

8, summary

UT is the developer’s weapon, is the development of the front umbrella, but also write a strong guarantee of robust code, in a word will not write UT development is not a good cook.