A conceptual summary of TDD was provided in test-Driven Development (TDD) Summary – Principles. In my opinion, the disadvantage of theoretical knowledge is that it only emphasizes external stimuli and lacks the internal psychological process of learners. For example, it is difficult to establish a mapping relationship of theoretical knowledge based on existing experience. Therefore, objective practice is the only criterion for testing truth. With this goal in mind, it takes some time to choose a case, because a good story or a good case can not only make me feel more relevant, but also exert the charm of TDD.

preface

The article includes case, task decomposition, count off part of the interface design, unit test naming rules and TDD case practice, about the source of the article has been put on my personal Github, I hope the next SERIES of TDD practice articles for readers also have some harvest.

The scope of

TDD (Test Driven Development) may have different understandings in different circles and different roles. Some people may understand it as ATDD (Acceptance Test Driven Development). Some people may also understand UTDD (Unit Test Driven Development). To avoid ambiguity, TDD refers specifically to UTDD (Unit Test Driven Development).

To prepare

  1. Understanding of OOP.
  2. Learn about Java 8.
  3. Familiar with Intellij IDEA.
  4. Familiar with the theoretical knowledge of TDD, you can refer to test Driven Development (TDD) summary – Principles.
  5. Learn about Guice, Google’s lightweight dependency injection framework.
  6. Familiar with test tools Junit and Mockito.
  7. Familiar with setting up automatic unit test environment.

case

You are a physical education teacher and you decide to play a game five minutes before the end of a class. There are 100 students in class at this time. The rules of the game are:

  1. You start by naming three different special numbers that must be digit numbers, such as 3, 5, and 7.
  2. Ask all the students to line up and count off in order.
  3. If the number given is a multiple of the first special number (3), students should say Fizz instead of that number. If the number reported is a multiple of the second special number (5), say Buzz; If the number reported is a multiple of the third special number (7), say Whizz.
  4. Student: If the number given is a multiple of two special numbers, the number should be treated as a multiple of the first special number and the second special number, then you should say FizzBuzz, etc. FizzBuzzWhizz if it’s a multiple of three special numbers.
  5. Fizz, Fizz, Fizz, Fizz, Fizz, Fizz, Fizz, Fizz, Fizz, Fizz If the number contains the first special number, ignore rules 3 and 4. For example, students who want to report 35 will only report Fizz, not “BuzzWhizz.”

Now, we need you to complete a program to simulate the game, which first accepts three special numbers and then outputs the numbers or words that 100 students should count. Such as:

Input:

3, 5, 7

Output (snippet) :

1, 2, Fizz, 4, Buzz, Fizz, Whizz, 8, Buzz, Fizz, 11, Fizz, Fizz, Whizz, FizzBuzz, 16 or 17, Fizz, 19, Buzz,… , 100,

Task decomposition

Requirements analysis prior to TDD can be done at the beginning of a clear goal to complete the task, so as to reduce the understanding of the error caused by low-level errors; The requirements are then broken down into tasks with the goal of getting a list of tasks that can be verified. As you practice TDD, you may also adjust the task list, adding new tasks, removing redundant tasks, and so on.

In the process of demand analysis, I will first analyze the roles involved in the whole case from the perspective of participants and find that there are two roles involved in the game, namely teachers and students. Then from the point of view of responsibility, the teacher’s responsibility is to initiate the game, define the rules of the game and say three non-repeating single-digit numbers; Students are responsible for participating in the game and counting according to the rules of the game. Finally, I came up with the following preliminary task list:

  1. Initiate the game.
  2. Define the rules of the game.
  3. Name three non-repeating single-digit numbers.
  4. Count off the students.
  5. Verify the input parameter.

Task breakdown

I found that the “student count” task was influenced by a set of rules of the game, so I refined the task and looked for the special needs of the task to facilitate some degree of planning for the task. In the analysis process, I will mark special or important rules to avoid forgetting them. Here’s my to-do list, broken down:

  1. Initiate the game.
  2. Define the rules of the game.
  3. Name three non-repeating single-digit numbers.
  4. !!!!!!!!!Count off the students.
    • If it is a multiple of the first special number, say Fizz.
    • If it’s a multiple of the second particular number, report Buzz.
    • If it is a multiple of the third special number, report Whizz.
    • If it is a multiple of multiple special digits at the same time, add the corresponding words in the order of the special digits, for example, FizzBuzz, BuzzWhizz, and FizzBuzzWhizz.
    • If the first special number is included, only Fizz is reported (ignoring rules 1, 2, 3, 4).
    • If it is not a multiple of a special digit and does not contain the first special digit, the corresponding ordinal number is reported.
  5. Verify the input parameter.

Mission planning

Sometimes the decomposed tasks do not reflect priorities and dependencies. In order to improve work efficiency, tasks need to be planned to make things more organized and avoid getting lost in a pile of tasks.

There is not much room for this step because of the average difficulty of the case, but in this step you can think about which task to start with, and choose the following three criteria:

  • The importance of the task
  • Task dependencies
  • Difficulty of the task

The importance of the task can be judged by judging whether the task is the main process. For example, the important procedure of “verification input” is lower than that of “student count” and can be done later.

The order of tasks can be identified by analyzing the dependencies of tasks. The specific priority varies from person to person. Some people prefer top-down, while others prefer bottom-up. The good news is that mocks help developers isolate dependencies, and they can also Mock out classes and interfaces that don’t depend on the implementation, and avoid the hassle of finding the sequence of tasks.

The difficulty of the analysis task needs to be determined through demand analysis and experience. Through the analysis of the above cases, it can be known that the difficulty lies in the algorithm of student count. For me personally, I don’t start with a difficult non-core task unless it’s a core task.

Finalize tasks

Through analysis, the difficult task “student count count” is the core function of the whole game, and I finally choose to do this task first.

Test naming convention

  1. The test class is named XXXTest.
  2. The test method name must be usedshould_xxx_when_xxx, such as:should_return_false_when_1_is_greater_than_2.
  3. The code logic of the test method follows the Given-when-then pattern.

Knowledge: Given the When – Then

When writing test methods, you should follow the Given-when-then pattern (Given XX, When doing XX, you will get XX feedback). This pattern allows developers to focus and think about these things:

  • Given: Drives us to think about what context the test is in and what objects to use, so that we can think about what context and objects to create.
  • When: Drives us to think from the user’s point of view about what the behavior is and what its inputs are, in order to think about method naming and input.
  • Then: What is the feedback that drives our thinking behavior in order to think about the return value of the method?

Consider: what is the significance of should_xxx_when_XXX?

Thanks to THE ideas and tools of BDD, this naming method was developed in my BDD practice (although I am not the only one using this naming method), and it contains but is not limited to the following advantages:

  • You can avoid getting bogged down in implementation details by focusing on behavior.
  • Naming close to natural language, clear expression intention, high readability, benefit from a wide range of people.
  • Good control of the scope of testing, from user behavior (BDD biased) to logical branching (TDD biased).

Now that the requirements are clear, the test naming conventions are drawn up, and the tasks are finalized, TDD is ready to begin.

Common mistakes

Early development habit (coding – run – observation) can lead to premature developers into the implementation details, one of the defects of this development habit is feedback cycle is long, is not conducive to the rhythm of the steps run, so in the process of practice TDD need moment remind myself that the slogan of TDD and rules, develop the new thinking habit.

Test-driven development

Finalize tasks

  • Count off the students.
    • If it is a multiple of the first special number, say Fizz.
    • If it’s a multiple of the second particular number, report Buzz.
    • If it is a multiple of the third special number, report Whizz.
    • If it is a multiple of multiple special digits at the same time, add the corresponding words in the order of the special digits, for example, FizzBuzz, BuzzWhizz, and FizzBuzzWhizz.
    • If the first special number is included, only Fizz is reported (ignoring rules 1, 2, 3, 4).
    • Fizz if it is not a multiple of a special number and does not contain the first special number.

Based on the overall flow of TDD, it’s time to think about what I’m going to do, how I’m going to test it, and then write a little test. Think about the required classes, interfaces, inputs, and outputs.

According to the previous demand analysis, students need to make clear their corresponding serial numbers and gameRules before they can count, so drive the Student class and String countOff(position, gameRules) method, observe countOff method and find that gameRules are needed. So you also drive the GameRule class.


Write enough code to make the test fail (it’s better to fail explicitly than to feel vague).

@Test
public void should_return_fizz_when_just_a_multiple_of_the_first_number(a) {
    List<GameRule> gameRules = new ArrayList<>();
    assertThat(Student.countOff(3, gameRules)).isEqualTo("Fizz");
}
Copy the code

This code failed to compile when it ran because it was missing the necessary classes and methods, so I quickly added the following code:

public class Student {
    public static String countOff(Integer position, List<GameRule> gameRules) {
        return ""; }}public class GameRule {}Copy the code

Then I ran the unit test and got the following error message:


Write just enough code to pass the test (make sure that any tests you write before also pass).

After checking for errors, I found that adding GameRule affected my judgment of the code’s obvious implementation, so at this point I used a pseudo-implementation strategy to get the test passed as quickly as possible in order to capture the obvious implementation in constant subtle feedback, so I quickly typed the following pseudo-code:

public static String countOff(Integer position, List<GameRule> gameRules) {
    return "Fizz";
}
Copy the code

Thankfully the test passed and I got the results I wanted very quickly:

I know this code is problematic, and now I’m wondering should I continue to write a GameRule to make the pseudo-implementation obvious? Or do you pick the next task and record “Write a GameRule” on your to-do list and so on? The criteria for this choice is simply to determine how long it will take to complete the task, and if it takes a while, move on, and if it takes a while, make a note of the next task. Through my analysis, I only needed to add two member variables (numbers and corresponding terms) to GameRule to achieve my goal, and THEN I adjusted the corresponding test code:

@Test
public void should_return_fizz_when_just_a_multiple_of_the_first_umbe(a) {
    List<GameRule> gameRules = Lists.list(
        new GameRule(3."Fizz"),
        new GameRule(5."Buzz"),
        new GameRule(7."Whizz")); assertThat(Student.countOff(3, gameRules)).isEqualTo("Fizz");
}
Copy the code

This is followed by the following code:

public class GameRule {
    private Integer number;
    private String term;

    public GameRule(Integer number, String term) {
        this.number = number;
        this.term = term; }... }public class Student {
    public static String countOff(Integer position, List<GameRule> gameRules) {
        if (position % gameRules.get(0).getNumber() == 0) {
            return gameRules.get(0).getTerm();
        }
        returnposition.toString(); }}Copy the code

Then run the test:


Because there is very little testing and code, and no obvious bad taste, there is no need to refactor for now, just cross out the current subtask and pick the next one. Due to the limited scope of the article, the overall process of repeating TDD for many times came after:

  • Count off the students.
    • If it is a multiple of the first special number, say Fizz.
    • If it’s a multiple of the second particular number, report Buzz.
    • If it is a multiple of the third special number, Whizz is called (current task).
    • If it is a multiple of multiple special digits at the same time, add the corresponding words in the order of the special digits, for example, FizzBuzz, BuzzWhizz, and FizzBuzzWhizz.
    • If the first special number is included, only Fizz is reported (ignoring rules 1, 2, 3, 4).
    • If it is not a multiple of a special digit and does not contain the first special digit, the corresponding ordinal number is reported.
public class StudentTest {

    private final List<GameRule> gameRules = Lists.list(
            new GameRule(3."Fizz"),
            new GameRule(5."Buzz"),
            new GameRule(7."Whizz"));@Test
    public void should_return_1_when_mismatch_any_number(a) {
        assertThat(Student.countOff(1, gameRules)).isEqualTo("1");
    }

    @Test
    public void should_return_fizz_when_just_a_multiple_of_the_first_number(a) {
        assertThat(Student.countOff(3, gameRules)).isEqualTo("Fizz");
        assertThat(Student.countOff(6, gameRules)).isEqualTo("Fizz");
    }

    @Test
    public void should_return_buzz_when_just_a_multiple_of_the_second_number(a) {
        assertThat(Student.countOff(5, gameRules)).isEqualTo("Buzz");
        assertThat(Student.countOff(10, gameRules)).isEqualTo("Buzz");
    }

    @Test
    public void should_return_whizz_when_just_a_multiple_of_the_third_number(a) {
        assertThat(Student.countOff(7, gameRules)).isEqualTo("Whizz");
        assertThat(Student.countOff(14, gameRules)).isEqualTo("Whizz"); }}public class Student {

    public static String countOff(Integer position, List<GameRule> gameRules) {
        if (position % gameRules.get(0).getNumber() == 0) {
            return gameRules.get(0).getTerm();
        } else if (position % gameRules.get(1).getNumber() == 0) {
            return gameRules.get(1).getTerm();
        } else if (position % gameRules.get(2).getNumber() == 0) {
            return gameRules.get(2).getTerm();
        }
        returnposition.toString(); }}Copy the code

At this point, the “bad taste” of the code becomes apparent, and a refactoring phase is introduced to eliminate repetitive design and make the pace of small steps more steady.

Read the series:

  • Test Driven Development (TDD) Summary – Principles

The source code

Github.com/lynings/tdd…


Welcome to follow my wechat subscription number, I will continue to output more technical articles, I hope we can learn from each other.