All of us at the time of development, may be encountered this kind of circumstance, want to start a development server, after performing the service startup command, the system prompts you to monitor the port is already being used Although modify a bottom slogan is not very troublesome, but is very uncomfortable, so I think have a tool to take up the process of port to kill directly.

With such a requirement, of course, to play the subjective initiative of programmers, their own implementation of it. Just recently saw some information about TDD, take this opportunity to practice, after all, the paper comes zhongjue shallow, must know this to practice.

Full source address github

What is a TDD

TDD, also known as test-driven Development, is a “test-first” programming methodology, whose basic process revolves around the cycle of Test- > code (reconstruction) -> Test.

TDD has three important principles:

  1. You are not allowed to write any production code except to pass a unit test.
  2. You are only allowed to write just enough to fail in a unit test.
  3. You can only write production code that passes one unit test at a time, no more.
  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

According to the three TDD principles, the development process is shown in the figure below

Will we follow these principles in our requirements development process to show how to develop our application using TDD

Demand analysis

Before we start coding, we do an analysis of the requirements, comb through the process, and identify business priorities for our development. So what are our needs? Specifically, it checks the port number entered by the user. If the port number is occupied, it displays information about the occupied process and asks the user whether to terminate the process.

According to the description of the requirement, the business process of the requirement is as follows:

  1. The user enters a port number
  2. Obtain information about occupied processes based on port numbers
  3. A message is displayed asking the user whether to end the process
  4. Terminates the user process and exits the program or exits directly, depending on the user’s choice

Organize test cases

According to the three PRINCIPLES of TDD, we need to write tests before starting to write code, so we need to sort out the test cases first. After combing through the current component requirements, we sorted out the following test cases:

  1. If the user enters a port number other than a number, a prompt message should be returned
  2. If no process that occupies the port is found, a message is displayed
  3. If the process that occupies the corresponding port is found, the process information should be returned
  4. If the user chooses to end the process, it should end the process and exit the program
  5. If the user chooses to exit the program directly, he should exit the program directly

With the use cases sorted out, we are ready to start coding

preparation

We developed the program in typescript and tested it using JEST, so we initialized our project workspace with NPM init-y and added dependencies

npm i typescript jest ts-jest @types/jest --dev
Copy the code

Generate typescript configuration files

npx typescript --init
Copy the code

Since ts was used for development, we used the TS-Jest plug-in to generate the configuration

npx ts-jest config:init
Copy the code

Modify the package.json file

  "scripts": {
+ "test": "jest"
- "test": "echo \"Error: no test specified\" && exit 1"
  },
Copy the code

Create a new SRC folder in the root directory and modify the tsconfig.json file

{... other code+ "include": ["./src/**/*.ts"]
}
Copy the code

So our preparatory work is finished.

Analyze dependencies and build abstract interfaces

Next we analyze the requirements and test cases, identify the dependencies, and create abstract interfaces to them. In general, external dependencies such as ———— network, I/O, and middleware, which are easiest to abstract, can be abstracted into interfaces.

When running, our program needs to receive the user’s Command line Input and Output text prompts. It also needs to obtain the information of the process occupied by the port number and end the process by executing external commands. Therefore, the corresponding Input, Output and Command interfaces are abstracted. The ProcessInfo interface is used to describe the data structure of the process information.

Create a new interfaces.ts file in the SRC folder and define the following interfaces

//interfaces.ts
export interface Input {
  receive(): Promise<string>;
}

export interface Output {
  write(text: string) :Promise<void>;
}

export interface Command {
  findProcessByPort(port: number): ProcessInfo | null;

  killProcess(process: ProcessInfo): void;
}

export interface ProcessInfo {
  pid: number;
  name: string;
}
Copy the code

Create handler.ts in the SRC folder, and then define the Handler class to inject the dependent interface through the constructor

// handler.ts
import { Command, Input, Output } from "./interfaces";

export class Handler {
  private input: Input;
  private output: Output;
  private command: Command;

  constructor(input: Input, output: Output, command: Command) {
    this.input = input;
    this.output = output;
    this.command = command;
  }

  async handle(): Promise<void> {}}Copy the code

Write the test -> test fails -> Code -> Pass the test -> Refactor

Now we’re ready to write our first test

1. Write the test

describe("Handler".() = > {
  test("should output tips if user input value not number".async() = > {const input: jest.Mocked<Input> = {
      receive: jest.fn()
    };
    const output: jest.Mocked<Output> = {
      write: jest.fn()
    };
    const command: jest.Mocked<Command> = {
      findProcessByPort: jest.fn(),
      killProcess: jest.fn()
    };
    const handler = new Handler(input, output, command);

    input.receive.mockResolvedValue("1024a");

    await handler.handle();

    expect(output.write).toBeCalledWith("The port number should be pure numbers. Please enter a valid port number.");
  });
});
Copy the code

2. Write code

Note that we only need to write just enough code to pass the test

export class Handler {
  /// omit other code
  async handle(): Promise<void> {
    const inputStr = await this.input.receive();
    const port = Number(inputStr);

    if (Number.isNaN(port)) {
      await this.output.write("The port number should be pure numbers. Please enter a valid port number.");
      return; }}}Copy the code

3. Pass the test

Run NPM run test

PASS  src/handler.test.ts
  Handler
    √ should output tips if user input value not number (8 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.762 s, estimated 3 s
Ran all test suites.
Copy the code

4. Continue writing tests

Very good! The test passed without a hitch, and now the second test

// handler.test.ts
test("should output tips message if port not be use".async() = > {const input: jest.Mocked<Input> = {
    receive: jest.fn()
  };
  const output: jest.Mocked<Output> = {
    write: jest.fn()
  };
  const command: jest.Mocked<Command> = {
    findProcessByPort: jest.fn(),
    killProcess: jest.fn()
  };
  const handler = new Handler(input, output, command);

  input.receive.mockResolvedValue("1024");
  command.findProcessByPort.mockReturnValue(null);

  await handler.handle();

  expect(command.findProcessByPort).toBeCalledWith(1024);
  expect(output.write).toBeCalledWith("Port number :1024 not used");
});
Copy the code

Again, we write code to pass the test

// handler.ts Handler.handle
const processInfo = this.command.findProcessByPort(port);

if (processInfo === null) {
  await this.output.write('Port number:${port}Not in use);
  return;
}
Copy the code
PASS  src/handler.test.ts
  Handler
    √ should output tips if user input value not number
    √ should output tips message if port not be use (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.364 s, estimated 1 s
Ran all test suites related to changed files.
Copy the code

5. Refactoring

Looking back at our test files, we found that the code reeked of decay. We had repeated the same test class construction process twice, and it was time to refactor.

const makeSut = () = > {
  const input: jest.Mocked<Input> = {
    receive: jest.fn()
  };
  const output: jest.Mocked<Output> = {
    write: jest.fn()
  };
  const command: jest.Mocked<Command> = {
    findProcessByPort: jest.fn(),
    killProcess: jest.fn()
  };
  const handler = new Handler(input, output, command);

  return {
    input,
    output,
    command,
    handler
  };
};

describe("Handler".() = > {
  let input: jest.Mocked<Input>;
  let output: jest.Mocked<Output>;
  let command: jest.Mocked<Command>;
  let handler: Handler;

  // called every time before test is executed
  beforeEach(() = > {
    const sut = makeSut();
    input = sut.input;
    output = sut.output;
    command = sut.command;
    handler = sut.handler;
  });

  test("should output tips if user input value not number".async() = > {}); test("should output tips message if port not be use".async() = > {}); });Copy the code

The test code is part of the source code and should be refactored constantly, otherwise the rot will spread throughout the project as the number of tests increases. Then you just keep writing tests, then writing business code to get the tests passed, looking for bad smells in the code, refactoring, and so on until all the test cases pass. See the source code for the complete test case, not expanded here.

conclusion

The above is my simple practice of applying TDD to a requirement. Applying TDD in the actual development process costs a little, and TDD is not a silver bullet. Whether to use TDD also needs to make a choice according to the requirements of the project and the composition of personnel.

reference

Test-Driven Development(TDD) in Go

Test-driven Development Practices – Test-Driven Development