[Node] Testing Journey

Unit, Integration, Performance Test & TDD

ยท

5 min read

[Node] Testing Journey

๐Ÿ“Œ jest Unit Test

What is Unit Testing?

Unit testing is a type of testing where individual units or components of a software system are tested. It focuses on validating the behavior and functionality of these units in an isolated and fast manner.

  • It covers basic business logic and exception cases.

  • It uses fake or mock objects instead of actual infrastructure components.

Objectives of Unit Testing

  • Minimize effort spent on testing while maximizing benefits.

  • Code tends to deteriorate over time and becomes more disorderly with each change. Therefore, test code is essential for ongoing maintenance.

  • Perform tests when making code changes and refactor tests when refactoring the production code.

Characteristics of a Successful Unit Test Suite

  • Tests should be automatically verifiable. Therefore, they should be integrated into the development cycle.

  • Focus on the most important parts, such as business logic and domain models.

Using Jest

  • The key is to redefine the functions used within each method, which is called mocking. Both models and functions can be mocked by mimicking the original function's behavior and specifying the return value for testing purposes using jest.fn(() => returnValue).

๐Ÿ“Œ supertest Integration Test

What is Integration Testing?

Integration testing is a type of testing that validates the interaction and behavior of multiple units or components as a whole. It ensures that the units work correctly when combined and handle the main flows and edge cases that unit tests may not cover. It involves using actual components that the application uses in production, requiring more code and data processing. It also requires setting up a test database.

Objectives of Integration Testing

  • Verify the interaction between modules.

  • Cover main flows and cases that unit tests cannot handle.

Using Jest and supertest

  1. Maintaining login state: To do this, you need to keep the cookies containing the tokens. After issuing the accessToken and refreshToken in the login API, you can store them in another object. Then, you can directly add the cookies to the headers for APIs that pass through the auth middleware, like this:
const response = await supertest(app)
  .post(`/api/worldcup`) // API's HTTP Method & URL
  .set("Authorization", `Bearer ${userData.accessToken}`) // Set Access Token in header
  .set("refreshtoken", `${userData.refreshToken}`) // Set Refresh Token in header
  .send(createWorldcupRequestBodyParams); // Request Body

This approach creates a new request each time the test is run.

  1. Using agent() to maintain the request: The agent method allows the request to persist, similar to how a browser sends cookies by default.
describe('POST /api/auth/login', () => {
  const agent = request.agent(app);
  beforeEach((done) => {
    agent.post('/api/auth/login')
         .send({
           nickname: userData.nickname,
           password: userData.password,
           email: userData.email,
         })
         .end(done);
  });

  test("POST /api/worldcup", async () => {
    const createWorldcupRequestBodyParams = {
      title: worldcupData2.title,
      content: worldcupData2.content,
      choices: worldcupData2.choices,
    };
    const response = await agent
      .post(`/api/worldcup`)
      .send(createWorldcupRequestBodyParams);

    expect(response.status).toEqual(201)
        expect(response.body).toMatchObject({
              newWorldcup: {
                worldcup_id: worldcupData2.worldcup_id,
                user_id: userData.user_id,
                title: createWorldcupRequestBodyParams.title,
                content: createWorldcupRequestBodyParams.content,
                choices: createWorldcupRequestBodyParams.choices,
              },
            });
      });
});

๐Ÿ“Œ Artillery Load Test

What is Load Testing?

Load testing is a type of test that steadily increases the load until it reaches the threshold limit. It tests how much load the server can handle, essentially determining its capacity to handle requests.

  • While unit tests and integration tests can help identify syntax and logical issues in the code when it is actually deployed, it is challenging to predict how many simultaneous users or daily users the server can accommodate.

  • Even if there are no syntax or logical issues in the code, the service may still be interrupted due to hardware constraints of the server.

  • One common issue is Out of Memory (OOM), which occurs when the server allocates a certain amount of memory for each user to store their information. If the memory continuously increases and eventually exceeds the server's memory capacity, an OOM issue arises.

  • Load testing can help predict such issues to some extent.

Using Artillery

๐Ÿ“Œ TDD: Test-Driven Development

Difference from Traditional Process

What is TDD?

  • Test-Driven Development is based on the "Test-First" concept of eXtreme Programming (XP), which is one of the Agile methodologies.

    • XP is one of the Agile methodologies that focuses on continuously building prototypes without making extensive predictions about the future. This methodology allows real-time adaptation to additional requirements.

Red: Write a failing test. Green: Write the minimum code required to pass the test. Refactor: Refactor the code by removing duplication, generalizing, etc.

  • Key principles:
  1. Do not write any actual code until a failing test is written.

  2. Write the minimum amount of actual code required to pass the failing test.

Cases Where TDD is Appropriate = High Uncertainty

  • When starting a project for the first time (internal uncertainty)

  • When performing a project where customer requirements may change (external uncertainty)

  • When a significant amount of code needs to be changed during development

  • When unsure who will perform maintenance after development (external uncertainty)

Advantages of TDD

  • Production of robust, object-oriented code - TDD ensures code reusability and promotes thorough modularization by functionality.

  • Reduction in redesign time - TDD allows developers to clearly define what code/logic to write and start development.

  • Reduction in debugging time - Benefits of unit testing, making it easier to identify where problems occur in each layer.

  • Ease of additional implementation - TDD assumes automated unit testing, which shortens the testing period.

Disadvantages of TDD

  • Decreased productivity: TDD may slow down development speed. In practice, it takes about 10-30% more time compared to conventional development methods. Therefore, TDD is not widely adopted in systems integration (SI) projects.
ย