The matter has been decided

Robert C.Martin, in his book Professional Programmers, makes it clear:

The verdict on TDD, test-driven development, is uncontroversial

The author deeply believes this, but this is not a careless conclusion, also not because who said that he is right, this is based on the author’s own experience in TDD come out of some of the conclusions. In addition, some of the details of TDD may differ from Robert C.Martin’s views, which will be explained in a subsequent article devoted to TDD. But I believe in TDD as a whole.

2 I and TDD

In recent years, my focus has been less on back-end development and more on mobile and TypeScript and React front-end and desktop development.

Unfortunately, when I first started working on Android, I did not have the mentality to implement TDD. Then I took charge of iOS, but I took over a ready-made code and did not start from scratch, so I never thought about implementing TDD at all.

In 2020, when I was developing TypeScript and React desktop, ALTHOUGH I successfully applied the style of dome-driven thinking into this project, I did not implement TDD. Although I knew that the front-end testing framework jEST was available, considering the time and the first time I tried to use the front-end technology stack, Factors such as the maturity of technology mastery do not apply TDD to this.

Fortunately, IN the past two years, I have tried to apply TDD completely in a project of my company in 2011 and a part-time project of my own in 2012, so I have gained some experience based on it. I also strengthened my belief in TDD.

2.1 TDD practice project experience

2.1.1 TDD practice in 2019

In nineteen nineteen, I was in charge of a technical mid-stage project in the company for a period of time. Since the project was not large, the company asked the author to take charge of the back-end development of the project alone. At that time I had just come out of mobile development and had not done back-end development in some time. So in development, we thought about what technology to use and how to do it. Spring Boot is still selected to complete this project, because after all, Spring Boot’s stability and reliability can be trusted.

Since the author was in charge of the project by himself at that time, he could operate freely in a wide range of technologies without considering the acceptance of other colleagues or team members, so he tried to apply TDD to this project completely for the first time. That’s a good start. Good results have been achieved.

As shown in the figure, the author’s unit test coverage in the 19-year project was about 78.8%

2.1.2 TDD practice in 2020

In 20 years, due to the need to develop a system for my family to be applied to the company’s business, I paid more attention to the quality. Based on 19 years of experience, I applied TDD together with the dome-driven design concept to this project again. The overall feeling is still very good.

And this time, their quality requirements for all aspects higher.

2.2 Some experiences from TDD practice

Although the number of projects is small, only one per year, it has had a significant impact on my programming philosophy. By now, I have become convinced that TDD is very effective, and that it is something that a good programmer must and should do.

Here are some of my own thoughts

2.2.1 TDD is the only way to speed up coding

In fact, one of the biggest problems we encounter as programmers is the biggest contradiction of technology.

Here’s the paradox: We’re doing it, but a lot of the time we’re not making the decisions.

I believe that we will more or less encounter some scenarios, such as customers, leaders, or project managers, or product managers, I call these people collectively the technical layman, this group of people do not understand the technology, but can make decisions for us all the time. Of course, there’s nothing wrong with them making decisions about product form or other aspects, but a lot of times one of the things that they think they understand but don’t really have much of an idea of is how long it takes to make decisions for us.

This can be done in 2 weeks and must be done

Although the programmer feels that 2 weeks is essentially impossible to complete, it is often the time when the programmer has little to say. Technology cannot be more important than sales, commitment to customers, or any other conceivable reason. This is a dilemma that programmers face throughout their careers and cannot escape.

Most of the time, the subconscious decision of many programmers is to sacrifice the invisible quality of code in order to accelerate the visible quality of code.

Only our technicians can also understand, also a look much the same in function to run two copies of the code, can differ in terms of quality of invisible giant, is behind many programs to more and more difficult to push the key reason that is not visible on the quality of continuous sacrifice so that more and more difficult to sustain.

TDD is the only way to solve and improve this problem, but unfortunately, I found that most domestic programmers do not follow this method at all, and many programmers themselves agree with a point of view:

Writing unit tests lengthens the time it takes to complete functionality

Although I think these programmers probably didn’t implement it at all, they’re just feeling it. But in this philosophy, even the programmers themselves believe that, and it is unlikely that a group of technical laymen will agree with this philosophy, so the unit testing is never a matter of concern.

But in fact, from the author’s practical experience, this is a fundamentally untenable conclusion. In fact, I’ve found that there’s no better way to speed up code development than by writing unit tests. And I think a good programmer needs only a little time, can adapt to and quickly familiar with the work of unit testing.

Of course, this article isn’t about TDD in detail, so I’ll leave it at that and focus on why TDD speeds up code development.

2.2.2 Keep unit tests small and fast

Complete testing for a project or product includes many dimensions, including unit testing, integration testing, professional black and white box testing, performance stress testing, etc. Unit testing is an obvious way for programmers to validate their own code, and it needs to be small and fast enough to differentiate between other tests.

If our project or product is like building a house, the purpose of unit testing is to ensure the quality of each brick, that’s what unit testing is for. Use unit testing to ensure the quality of each brick, and then there is the possibility of a good house behind it.

So, the point of unit retries is to focus on the correctness of every logic you write. In code, make sure that every method you write is logically correct. If the logic of every method in the code is correct, there is likely to be a subsequent quality assurance that integrates these methods. Otherwise, it is as if a house is built on unreliable bricks.

2.2.3 Make good use of tools or technical frameworks

In fact, in some of the technical choices for coding, I usually put the ease of writing unit tests based on that technology as an important consideration.

For example, IN Java back-end development, I usually prefer JPA over Mybatis or other JDBC technologies, which may have slight performance advantages, but JPA is clearly better in terms of maintainability and ease of supporting unit testing.

I generally use H2 memory database, as a standard database unit tests, one of its biggest advantage is that can be in any environment, any time, without the need for a similar MySQL service support there, and I can set it each time a unit test database is a brand new this scenario to test. In this way, the logical correctness of your method can be tested with less interference.

If you think that this kind of test doesn’t reflect the reality, which is probably a lot of data, then again, testing has many dimensions, and unit tests don’t focus on the dimensions that you’re worried about.

Another important tool is Sonarqube, which is a great open source software, as I’ve shown in the two pictures above.

In the concept of agile software development, pair programming is a very important point, of course, this point is basically I think it is unlikely to be implemented in China, this mode will make leaders think that two technicians do one person’s work, it is hard to imagine our domestic decision makers will agree with this method.

Therefore, I think there are two alternatives in China:

  1. Use code review instead of pair programming
  2. Use an automated tool like Sonar

The first doesn’t work well in many domestic environments. So I basically just looked at the second one, which is to put your code on Sonar and have it tell me where I’m not doing well, what unit test coverage is, what code isn’t covered and so on. While many of its rules are dead and inflexible, it does at least check your code to a certain extent, especially when it comes to unit tests to remind you if you’re doing enough.

So, if you’re going to apply TDD, you’re going to need tools like this.

2.2.4 Learn to use mocks or stakes

Another important aspect of unit testing is learning to Mock, or pile, which varies from language to language but roughly means to simulate an implementation concept. Many times our code depends on the actual performance of some third party or something we don’t care about in another dimension in this test. In the unit test scenario, we need to cover the following scenarios:

  • Assuming a third party function returns to normal, what is our code logic like

  • Suppose a third party function returns an error. What about our code logic

This is where Mock techniques are needed. Often languages have similar frameworks; you just have to look for them. In the back-end Java family, the most famous, and the one used by this author, is Mockito.

    @Test
    void testDisableDocument(a){
        String json = "{\"name\":\"AAA.mp3\",\"mediaId\":\"AAA\"}";
        ResponseEntity<BaseResponse<DocumentDTO>> responseEntity = restTemplate.exchange(baseUrl() + "/v1/documents",HttpMethod.POST,createHttpEntityFromString(json),new ParameterizedTypeReference<>() {});
        Assertions.assertTrue(responseEntity.getStatusCode().is2xxSuccessful());
        Assertions.assertTrue(Objects.requireNonNull(responseEntity.getBody()).getResult().getId() > 0);

        // This is a MOCK that assumes that the current user is the super administrator and can disable documents
        Mockito.when(applicationAuth.isSuper()).thenReturn(true);
        ResponseEntity<BaseResponse> deleteResponseEntity = restTemplate.exchange(baseUrl() + "/v1/documents/" + responseEntity.getBody().getResult().getId(), HttpMethod.DELETE, createEmptyHttpEntity(), new ParameterizedTypeReference<>() {});
        Assertions.assertTrue(deleteResponseEntity.getStatusCode().is2xxSuccessful());
        Assertions.assertTrue(Objects.requireNonNull(deleteResponseEntity.getBody()).isResultSuccess());


        // This is another Mock, assuming that the current user is not the super administrator and should not be able to access the document
        Mockito.when(applicationAuth.isSuper()).thenReturn(false);
        deleteResponseEntity = restTemplate.exchange(baseUrl() + "/v1/documents/" + responseEntity.getBody().getResult().getId(), HttpMethod.DELETE, createEmptyHttpEntity(), new ParameterizedTypeReference<>() {});
        Assertions.assertTrue(deleteResponseEntity.getStatusCode().is2xxSuccessful());
        Assertions.assertFalse(Objects.requireNonNull(deleteResponseEntity.getBody()).isResultSuccess());
    }
Copy the code

As shown in the code above, using Mock wisely makes your unit tests more pure, focusing only on the correctness of the logic of the code being tested, and shielding it from the influence of other related logic.

Another very big advantage is that it makes unit tests very small and pure. To run a unit test without a similar Mock framework, I need a complete permission system to run the code, which is very cumbersome and makes the unit test very heavy and uncontrollable.

2.2.5 Unit tests need to consider normal and abnormal paths

In the early days, I wrote unit tests and basically just wrote normal paths. What is a normal path? It just goes all the way down, and it turns out fine. For example, if you add a user, the new user succeeds. This is called the normal path.

Later I realized that there was very little coverage, so I started trying to add abnormal paths. This results in better unit tests.

This code is extracted from the author's MyDDD-VerTX framework, based on vert. x and Kotlin's responsive domain driver implementation


    @Test
    fun testRefreshToken(testContext: VertxTestContext){
        executeWithTryCatch(testContext){
            GlobalScope.launch {
                
                // Abnormal path, empty user must not be allowed to refresh Token
                try {
                    oAuth2Auth.refresh(null).await()
                    testContext.failNow("Empty user cannot refresh TOKEN")}catch (e:Exception){
                    testContext.verify { Assertions.assertNotNull(e) }
                }
                
                // Abnormal path. The user is incorrect. Token refresh is not allowed
                try {
                    oAuth2Auth.refresh(OAuth2UserDTO()).await()
                    testContext.failNow("Incorrect User cannot refresh TOKEN")}catch (e:Exception){
                    testContext.verify { Assertions.assertNotNull(e) }
                }

                val createdClient = createClient().createClient().await()
                val user = oAuth2Auth.authenticate(JsonObject().put("clientId",createdClient.clientId)
                    .put("clientSecret",createdClient.clientSecret)).await()
                
                // The Token can be refreshed by the correct user
                val token = oAuth2Auth.refresh(user).await()
                testContext.verify {
                    Assertions.assertNotNull(token)
                }
                
                // abnormal path, refreshToken is incorrect, cannot refreshToken
                try {
                    val oauthUser = user asOAuth2UserDTO oauthUser.tokenDTO? .refreshToken = UUID.randomUUID().toString() oAuth2Auth.refresh(oauthUser).await() testContext.failNow("Incorrect refreshToken cannot refreshToken")}catch (e:Exception){
                    testContext.verify { Assertions.assertNotNull(e) }
                }

                testContext.completeNow()
            }
        }
    }

Copy the code

As the code above shows, writing unit tests takes into account different conditions.

Let TDD drive my code

Thanks to the practical experience of several projects and good results, I now agree with TDD very much.

So, starting in 2021, my commitment to TDD is as follows:

Your own project should have no less than 80% coverage, and if it’s a company project, it depends on how much control you actually have.

The following shows the data for myDDD-Vertx, a responsive domain-driven implementation based on vert. x and Kotlin, that I am working on.

I write this article in the hope that more programmers in China will try to practice TDD, and only in this way can the quality and maintainability of code be improved more and more.

Nothing else!!


For more quality articles, please visit the author’s personal website: LingenLiu. cc or official account: [Royal Sword Xuan] – dedicated to practice and spread elegant coding