Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      From Data To Decisions: UX Strategies For Real-Time Dashboards

      September 13, 2025

      Honeycomb launches AI observability suite for developers

      September 13, 2025

      Low-Code vs No-Code Platforms for Node.js: What CTOs Must Know Before Investing

      September 12, 2025

      ServiceNow unveils Zurich AI platform

      September 12, 2025

      DistroWatch Weekly, Issue 1139

      September 14, 2025

      Building personal apps with open source and AI

      September 12, 2025

      What Can We Actually Do With corner-shape?

      September 12, 2025

      Craft, Clarity, and Care: The Story and Work of Mengchu Yao

      September 12, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      Optimizely Mission Control – Part III

      September 14, 2025
      Recent

      Optimizely Mission Control – Part III

      September 14, 2025

      Learning from PHP Log to File Example

      September 13, 2025

      Online EMI Calculator using PHP – Calculate Loan EMI, Interest, and Amortization Schedule

      September 13, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      DistroWatch Weekly, Issue 1139

      September 14, 2025
      Recent

      DistroWatch Weekly, Issue 1139

      September 14, 2025

      sudo vs sudo-rs: What You Need to Know About the Rust Takeover of Classic Sudo Command

      September 14, 2025

      Dmitry — The Deep Magic

      September 13, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»How to Write Unit Tests and E2E Tests for NestJS Applications

    How to Write Unit Tests and E2E Tests for NestJS Applications

    April 16, 2025

    Recently, I have been writing unit tests and E2E tests for a NestJS project. This was my first time writing tests for a backend project, and I found the process different from my experience with frontend testing, making it challenging to begin.

    After looking at some examples, I have gained a clearer understanding of how to approach testing. So I wrote an article to record and share my learning to help others who may be facing similar confusion.

    In addition, I have put together a demo project with the relevant unit and E2E tests completed, which may be of interest. I’ve uploaded the code to Github here.

    Table of Contents

    1. Prerequisites

    2. Difference Between Unit Testing and E2E Testing

    3. Writing Unit Tests

      • The First Test Case

      • The Second Test Case

      • Unit Test Coverage

    4. Writing E2E Tests

    5. Whether to Write Tests

      • Enhancing System Robustness

      • Enhancing Maintainability

      • Enhancing Development Efficiency

    6. When Not to Write Tests?

    7. Conclusion

    8. Reference Materials

    Prerequisites

    Before diving into this tutorial, you should have:

    • Basic knowledge of TypeScript and Node.js

    • Familiarity with NestJS fundamentals

    • Understanding of RESTful APIs

    • MongoDB installed (as the example uses MongoDB)

    • Node.js and npm/yarn installed on your system

    • Basic understanding of testing concepts

    You can find the complete code examples in the demo repository. You can clone it to follow along with the examples.

    Difference Between Unit Testing and E2E Testing

    Unit tests and E2E tests are methods of software testing, but they have different goals and scopes.

    Unit testing involves checking and verifying the smallest testable unit within the software. A function or a method, for example, can be considered a unit. In unit testing, you provide expected outputs for various inputs of a function and validate the correctness of its operation. The goal of unit testing is to quickly identify bugs within the function, and they are easy to write and execute rapidly.

    On the other hand, E2E tests often simulate real-world user scenarios to test the entire application. For instance, the frontend typically uses a browser or headless browser for testing, while the backend does so by simulating API calls.

    Within a NestJS project, unit tests might assess a specific service or a method of a controller, such as verifying if the update method in the Users module correctly updates a user. An E2E test, however, may examine a complete user journey, from creating a new user to updating their password and eventually deleting the user, which involves multiple services and controllers.

    How to Write Unit Tests

    Writing unit tests for a utility function or method that doesn’t involve interfaces is relatively straightforward. You only need to consider the various inputs and write the corresponding test code. But the situation becomes more complex once interfaces come into play. Let’s use code as an example:

    <span class="hljs-keyword">async</span> validateUser(
      username: <span class="hljs-built_in">string</span>,
      password: <span class="hljs-built_in">string</span>,
    ): <span class="hljs-built_in">Promise</span><UserAccountDto> {
      <span class="hljs-keyword">const</span> entity = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.usersService.findOne({ username });
      <span class="hljs-keyword">if</span> (!entity) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(<span class="hljs-string">'User not found'</span>);
      }
      <span class="hljs-keyword">if</span> (entity.lockUntil && entity.lockUntil > <span class="hljs-built_in">Date</span>.now()) {
        <span class="hljs-keyword">const</span> diffInSeconds = <span class="hljs-built_in">Math</span>.round((entity.lockUntil - <span class="hljs-built_in">Date</span>.now()) / <span class="hljs-number">1000</span>);
        <span class="hljs-keyword">let</span> message = <span class="hljs-string">`The account is locked. Please try again in <span class="hljs-subst">${diffInSeconds}</span> seconds.`</span>;
        <span class="hljs-keyword">if</span> (diffInSeconds > <span class="hljs-number">60</span>) {
          <span class="hljs-keyword">const</span> diffInMinutes = <span class="hljs-built_in">Math</span>.round(diffInSeconds / <span class="hljs-number">60</span>);
          message = <span class="hljs-string">`The account is locked. Please try again in <span class="hljs-subst">${diffInMinutes}</span> minutes.`</span>;
        }
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(message);
      }
      <span class="hljs-keyword">const</span> passwordMatch = bcrypt.compareSync(password, entity.password);
      <span class="hljs-keyword">if</span> (!passwordMatch) {
        <span class="hljs-comment">// $inc update to increase failedLoginAttempts</span>
        <span class="hljs-keyword">const</span> update = {
          $inc: { failedLoginAttempts: <span class="hljs-number">1</span> },
        };
        <span class="hljs-comment">// lock account when the third try is failed</span>
        <span class="hljs-keyword">if</span> (entity.failedLoginAttempts + <span class="hljs-number">1</span> >= <span class="hljs-number">3</span>) {
          <span class="hljs-comment">// $set update to lock the account for 5 minutes</span>
          update[<span class="hljs-string">'$set'</span>] = { lockUntil: <span class="hljs-built_in">Date</span>.now() + <span class="hljs-number">5</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span> };
        }
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.usersService.update(entity._id, update);
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(<span class="hljs-string">'Invalid password'</span>);
      }
      <span class="hljs-comment">// if validation is sucessful, then reset failedLoginAttempts and lockUntil</span>
      <span class="hljs-keyword">if</span> (
        entity.failedLoginAttempts > <span class="hljs-number">0</span> ||
        (entity.lockUntil && entity.lockUntil > <span class="hljs-built_in">Date</span>.now())
      ) {
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.usersService.update(entity._id, {
          $set: { failedLoginAttempts: <span class="hljs-number">0</span>, lockUntil: <span class="hljs-literal">null</span> },
        });
      }
      <span class="hljs-keyword">return</span> { userId: entity._id, username } <span class="hljs-keyword">as</span> UserAccountDto;
    }
    

    The code above is a method validateUser in the auth.service.ts file, primarily used to verify whether the username and password entered by the user during login are correct. It contains the following logic:

    1. Check if the user exists based on username. If not, throw a 401 exception (a 404 exception is also feasible).

    2. See if the user is locked out. If so, throw a 401 exception with a relevant message.

    3. Encrypt the password and compare it with the password in the database. If it’s incorrect, throw a 401 exception (three consecutive failed login attempts will lock the account for 5 minutes).

    4. If the login is successful, clear any previously failed login attempt counts (if applicable) and return the user id and username to the next stage.

    As you can see, the validateUser method includes four processing logics. So we need to write corresponding unit test code for these four points to ensure that the entire validateUser function is operating correctly.

    The First Test Case

    When we start writing unit tests, we encounter a problem: the findOne method needs to interact with the database, and it looks for corresponding users in the database through username. But if every unit test has to interact with the database, the testing will become very cumbersome. So we can mock fake data to achieve this.

    For example, assume we have registered a user named woai3c. Then, during login, the user data can be retrieved in the validateUser method through const entity = await this.usersService.findOne({ username });. As long as this line of code can return the desired data, there is no problem, even without database interaction. We can achieve this through mock data.

    Now, let’s look at the relevant test code for the validateUser method:

    <span class="hljs-keyword">import</span> { Test } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/testing'</span>;
    <span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/modules/auth/auth.service'</span>;
    <span class="hljs-keyword">import</span> { UsersService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/modules/users/users.service'</span>;
    <span class="hljs-keyword">import</span> { UnauthorizedException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
    <span class="hljs-keyword">import</span> { TEST_USER_NAME, TEST_USER_PASSWORD } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tests/constants'</span>;
    describe(<span class="hljs-string">'AuthService'</span>, <span class="hljs-function">() =></span> {
      <span class="hljs-keyword">let</span> authService: AuthService; <span class="hljs-comment">// Use the actual AuthService type</span>
      <span class="hljs-keyword">let</span> usersService: Partial<Record<keyof UsersService, jest.Mock>>;
      beforeEach(<span class="hljs-keyword">async</span> () => {
        usersService = {
          findOne: jest.fn(),
        };
        <span class="hljs-keyword">const</span> <span class="hljs-keyword">module</span> = await Test.createTestingModule({
          providers: [        AuthService,
            {
              provide: UsersService,
              useValue: usersService,
            },
          ],
        }).compile();
        authService = <span class="hljs-built_in">module</span>.get<AuthService>(AuthService);
      });
      describe(<span class="hljs-string">'validateUser'</span>, <span class="hljs-function">() =></span> {
        it(<span class="hljs-string">'should throw an UnauthorizedException if user is not found'</span>, <span class="hljs-keyword">async</span> () => {
          <span class="hljs-keyword">await</span> expect(
            authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
          ).rejects.toThrow(UnauthorizedException);
        });
        <span class="hljs-comment">// other tests...</span>
      });
    });
    

    We get the user data by calling the findOne method of usersService, so we need to mock the findOne method of usersService in the test code:

    beforeEach(<span class="hljs-keyword">async</span> () => {
        usersService = {
          findOne: jest.fn(), <span class="hljs-comment">// mock findOne method</span>
        };
        <span class="hljs-keyword">const</span> <span class="hljs-keyword">module</span> = await Test.createTestingModule({
          providers: [        AuthService, <span class="hljs-comment">// real AuthService, because we are testing its methods</span>
            {
              provide: UsersService, <span class="hljs-comment">// use mock usersService instead of real usersService</span>
              useValue: usersService,
            },
          ],
        }).compile();
        authService = <span class="hljs-built_in">module</span>.get<AuthService>(AuthService);
      });
    

    We use jest.fn() to return a function to replace the real usersService.findOne(). If usersService.findOne() is called now, there will be no return value, so the first unit test case will pass:

    it(<span class="hljs-string">'should throw an UnauthorizedException if user is not found'</span>, <span class="hljs-keyword">async</span> () => {
      <span class="hljs-keyword">await</span> expect(
        authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
      ).rejects.toThrow(UnauthorizedException);
    });
    

    Since findOne in const entity = await this.usersService.findOne({ username }); of the validateUser method is a mocked fake function with no return value, the 2nd to 4th lines of code in the validateUser method could execute:

    <span class="hljs-keyword">if</span> (!entity) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(<span class="hljs-string">'User not found'</span>);
    }
    

    It throws a 401 error, which is as expected.

    The Second Test Case

    The second logic in the validateUser method is to determine if the user is locked, with the corresponding code as follows:

    <span class="hljs-keyword">if</span> (entity.lockUntil && entity.lockUntil > <span class="hljs-built_in">Date</span>.now()) {
      <span class="hljs-keyword">const</span> diffInSeconds = <span class="hljs-built_in">Math</span>.round((entity.lockUntil - <span class="hljs-built_in">Date</span>.now()) / <span class="hljs-number">1000</span>);
      <span class="hljs-keyword">let</span> message = <span class="hljs-string">`The account is locked. Please try again in <span class="hljs-subst">${diffInSeconds}</span> seconds.`</span>;
      <span class="hljs-keyword">if</span> (diffInSeconds > <span class="hljs-number">60</span>) {
        <span class="hljs-keyword">const</span> diffInMinutes = <span class="hljs-built_in">Math</span>.round(diffInSeconds / <span class="hljs-number">60</span>);
        message = <span class="hljs-string">`The account is locked. Please try again in <span class="hljs-subst">${diffInMinutes}</span> minutes.`</span>;
      }
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(message);
    }
    

    As you can see, we can determine that the current account is locked if there is a lock time lockUntil in the user data and the lock end time is greater than the current time. So we need to mock a user data with the lockUntil field:

    it(<span class="hljs-string">'should throw an UnauthorizedException if the account is locked'</span>, <span class="hljs-keyword">async</span> () => {
      <span class="hljs-keyword">const</span> lockedUser = {
        _id: TEST_USER_ID,
        username: TEST_USER_NAME,
        password: TEST_USER_PASSWORD,
        lockUntil: <span class="hljs-built_in">Date</span>.now() + <span class="hljs-number">1000</span> * <span class="hljs-number">60</span> * <span class="hljs-number">5</span>, <span class="hljs-comment">// The account is locked for 5 minutes</span>
      };
      usersService.findOne.mockResolvedValueOnce(lockedUser);
      <span class="hljs-keyword">await</span> expect(
        authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
      ).rejects.toThrow(UnauthorizedException);
    });
    

    In the test code above, an object lockedUser is first defined, which contains the lockUntil field we need. Then, it is used as the return value for findOne, achieved by usersService.findOne.mockResolvedValueOnce(lockedUser);. Thus, when the validateUser method is executed, the user data within it is the mocked data, successfully allowing the second test case to pass.

    Unit Test Coverage

    Unit test coverage (Code Coverage) is a metric used to describe how much of the application code has been covered or tested by unit tests. It is typically expressed as a percentage, indicating how much of all possible code paths have been covered by test cases.

    Unit test coverage usually includes the following types:

    • Line Coverage: How many lines of code are covered by the tests.

    • Function Coverage: How many functions or methods are covered by the tests.

    • Branch Coverage: How many code branches are covered by the tests (for example, if/else statements).

    • Statement Coverage: How many statements in the code are covered by the tests.

    Unit test coverage is an important metric to measure the quality of unit tests, but it is not the only metric. A high coverage rate can help to detect errors in the code, but it does not guarantee the quality of the code. A low coverage rate may mean that there is untested code, potentially with undetected errors.

    The image below shows the unit test coverage results for a demo project:

    Unit test coverage overview showing test results with percentages for statements, branches, functions, and lines

    For files like services and controllers, it’s generally better to have a higher unit test coverage, while for files like modules there’s no need to write unit tests – nor is it possible to write them, as it’s meaningless.

    This is because NestJS modules are primarily configuration files that define the structure of your application by connecting controllers, services, and other components together. They don’t contain actual business logic to test, but rather serve as wiring instructions for the dependency injection system. Testing modules would only verify that NestJS’s core functionality works correctly, which is already tested by the NestJS team themselves.

    The above image represents the overall metrics for the entire unit test coverage. If you want to view the test coverage for a specific function, you can open the coverage/lcov-report/index.html file in the project’s root directory. For example, I want to see the specific test situation for the validateUser method:

    Detailed test coverage for the validateUser method showing specific uncovered lines highlighted in red

    As you can see, the original unit test coverage for the validateUser method is not 100%, and there are still two lines of code that were not executed. But it doesn’t matter much, as it does not affect the four key processing nodes, and we shouldn’t pursue high test coverage unidimensionally.

    How to Write E2E Tests

    In the unit tests section, you learned how to write unit tests for each feature of the validateUser() function, using mocked data to ensure that each feature could be tested.

    In e2E testing, we need to simulate real user scenarios, so connecting to a database for testing is necessary. So, the methods in the auth.service.ts module that we’ll be testing all interact with the database.

    The auth module primarily includes the following features:

    • Registration

    • Login

    • Token refresh

    • Reading user information

    • Changing password

    • Deleting users

    E2E tests need to test these six features one by one, starting with registration and ending with deleting users. During testing, we can create a dedicated test user to conduct the tests and then delete this test user upon completion, so we don’t leave any unnecessary information in the test database.

    beforeAll(<span class="hljs-keyword">async</span> () => {
      <span class="hljs-keyword">const</span> moduleFixture: TestingModule = <span class="hljs-keyword">await</span> Test.createTestingModule({
        imports: [AppModule],
      }).compile()
      app = moduleFixture.createNestApplication()
      <span class="hljs-keyword">await</span> app.init()
      <span class="hljs-comment">// Perform a login to obtain a token</span>
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> request(app.getHttpServer())
        .post(<span class="hljs-string">'/auth/register'</span>)
        .send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD })
        .expect(<span class="hljs-number">201</span>)
      accessToken = response.body.access_token
      refreshToken = response.body.refresh_token
    })
    afterAll(<span class="hljs-keyword">async</span> () => {
      <span class="hljs-keyword">await</span> request(app.getHttpServer())
        .delete(<span class="hljs-string">'/auth/delete-user'</span>)
        .set(<span class="hljs-string">'Authorization'</span>, <span class="hljs-string">`Bearer <span class="hljs-subst">${accessToken}</span>`</span>)
        .expect(<span class="hljs-number">200</span>)
      <span class="hljs-keyword">await</span> app.close()
    })
    

    The beforeAll hook function runs before all tests begin, so we can register a test account TEST_USER_NAME here. The afterAll hook function runs after all tests end, so it’s suitable to delete the test account TEST_USER_NAME here. It also conveniently tests the registration and deletion functions.

    In the previous section’s unit tests, we wrote relevant unit tests around the validateUser method. Actually, this method is executed during login to validate if the user’s account and password are correct. So this E2E test will also use the login process to demonstrate how to compose the E2E test cases.

    The entire login test process includes five small tests:

    describe(<span class="hljs-string">'login'</span>, <span class="hljs-function">() =></span> {
        it(<span class="hljs-string">'/auth/login (POST)'</span>, <span class="hljs-function">() =></span> {
          <span class="hljs-comment">// ...</span>
        })
        it(<span class="hljs-string">'/auth/login (POST) with user not found'</span>, <span class="hljs-function">() =></span> {
          <span class="hljs-comment">// ...</span>
        })
        it(<span class="hljs-string">'/auth/login (POST) without username or password'</span>, <span class="hljs-keyword">async</span> () => {
          <span class="hljs-comment">// ...</span>
        })
        it(<span class="hljs-string">'/auth/login (POST) with invalid password'</span>, <span class="hljs-function">() =></span> {
          <span class="hljs-comment">// ...</span>
        })
        it(<span class="hljs-string">'/auth/login (POST) account lock after multiple failed attempts'</span>, <span class="hljs-keyword">async</span> () => {
          <span class="hljs-comment">// ...</span>
        })
      })
    

    These five tests are as follows:

    1. Successful login, return 200

    2. If the user does not exist, throw a 401 exception

    3. If password or username is not provided, throw a 400 exception

    4. Login with the wrong password, throw a 401 exception

    5. If the account is locked, throw a 401 exception

    Now let’s start writing the E2E tests:

    <span class="hljs-comment">// login success</span>
    it(<span class="hljs-string">'/auth/login (POST)'</span>, <span class="hljs-function">() =></span> {
      <span class="hljs-keyword">return</span> request(app.getHttpServer())
        .post(<span class="hljs-string">'/auth/login'</span>)
        .send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD })
        .expect(<span class="hljs-number">200</span>)
    })
    <span class="hljs-comment">// if user not found, should throw 401 exception</span>
    it(<span class="hljs-string">'/auth/login (POST) with user not found'</span>, <span class="hljs-function">() =></span> {
      <span class="hljs-keyword">return</span> request(app.getHttpServer())
        .post(<span class="hljs-string">'/auth/login'</span>)
        .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
        .expect(<span class="hljs-number">401</span>) <span class="hljs-comment">// Expect an unauthorized error</span>
    })
    

    Writing E2E test code is relatively straightforward: you simply call the interface and then verify the result. For example, for the successful login test, we just need to verify that the returned result is 200.

    The first four tests are quite simple. Now let’s look at a slightly more complicated E2E test, which is to verify whether an account is locked.

    it(<span class="hljs-string">'/auth/login (POST) account lock after multiple failed attempts'</span>, <span class="hljs-keyword">async</span> () => {
      <span class="hljs-keyword">const</span> moduleFixture: TestingModule = <span class="hljs-keyword">await</span> Test.createTestingModule({
        imports: [AppModule],
      }).compile()
      <span class="hljs-keyword">const</span> app = moduleFixture.createNestApplication()
      <span class="hljs-keyword">await</span> app.init()
      <span class="hljs-keyword">const</span> registerResponse = <span class="hljs-keyword">await</span> request(app.getHttpServer())
        .post(<span class="hljs-string">'/auth/register'</span>)
        .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
      <span class="hljs-keyword">const</span> accessToken = registerResponse.body.access_token
      <span class="hljs-keyword">const</span> maxLoginAttempts = <span class="hljs-number">3</span> <span class="hljs-comment">// lock user when the third try is failed</span>
      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i < maxLoginAttempts; i++) {
        <span class="hljs-keyword">await</span> request(app.getHttpServer())
          .post(<span class="hljs-string">'/auth/login'</span>)
          .send({ username: TEST_USER_NAME2, password: <span class="hljs-string">'InvalidPassword'</span> })
      }
      <span class="hljs-comment">// The account is locked after the third failed login attempt</span>
      <span class="hljs-keyword">await</span> request(app.getHttpServer())
        .post(<span class="hljs-string">'/auth/login'</span>)
        .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
        .then(<span class="hljs-function">(<span class="hljs-params">res</span>) =></span> {
          expect(res.body.message).toContain(
            <span class="hljs-string">'The account is locked. Please try again in 5 minutes.'</span>,
          )
        })
      <span class="hljs-keyword">await</span> request(app.getHttpServer())
        .delete(<span class="hljs-string">'/auth/delete-user'</span>)
        .set(<span class="hljs-string">'Authorization'</span>, <span class="hljs-string">`Bearer <span class="hljs-subst">${accessToken}</span>`</span>)
      <span class="hljs-keyword">await</span> app.close()
    })
    

    When a user fails to log in three times in a row, the account will be locked. So in this test, we cannot use the test account TEST_USER_NAME, because if the test is successful, this account will be locked and unable to continue the following tests. We need to register another new user TEST_USER_NAME2 specifically to test account locking, and delete this user after the test is successful.

    So, as you can see, the code for this E2E test is quite substantial, requiring a lot of setup and teardown work, but the actual test code is just these few lines:

    <span class="hljs-comment">// login three times</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i < maxLoginAttempts; i++) {
      <span class="hljs-keyword">await</span> request(app.getHttpServer())
        .post(<span class="hljs-string">'/auth/login'</span>)
        .send({ username: TEST_USER_NAME2, password: <span class="hljs-string">'InvalidPassword'</span> })
    }
    <span class="hljs-comment">// test if the account is locked</span>
    <span class="hljs-keyword">await</span> request(app.getHttpServer())
      .post(<span class="hljs-string">'/auth/login'</span>)
      .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
      .then(<span class="hljs-function">(<span class="hljs-params">res</span>) =></span> {
        expect(res.body.message).toContain(
          <span class="hljs-string">'The account is locked. Please try again in 5 minutes.'</span>,
        )
      })
    

    Writing E2E test code is relatively simple. You don’t need to consider mock data or test coverage. It’s sufficient if the entire system process runs as expected.

    When to Write Tests

    If possible, I generally recommend writing tests. Doing so can enhance the robustness, maintainability, and development efficiency of the system. Here are some key reasons why writing tests is useful:

    Enhancing System Robustness

    When writing code, you usually focus on the program flow under normal inputs to ensure the core functionality works properly. But you might often overlook some edge cases, such as abnormal inputs.

    Writing tests changes this, as it forces you to consider how to handle these cases and respond appropriately, thus preventing crashes. So we can say that writing tests indirectly improves system robustness.

    Enhancing Maintainability

    Taking over a new project that includes comprehensive tests can be very pleasant. They act as a guide, helping you quickly understand the various functionalities. Just by looking at the test code, you can easily grasp the expected behavior and boundary conditions of each function without having to go through each line of the function’s code.

    Enhancing Development Efficiency

    Imagine a project that hasn’t been updated for a while suddenly receives new requirements. After making changes, you might worry about introducing bugs. Without tests, you would need to manually test the entire project again — wasting time and being inefficient.

    With complete tests, a single command can tell you whether the code changes have impacted existing functionalities. Even if there are errors, you can quickly locate them and address them.

    When Not to Write Tests

    For short-term projects and projects with very fast requirement iterations, it’s not recommended to write tests.

    For example, a project built for an event that will be useless after the event ends doesn’t need tests. Also, for projects that undergo very fast requirement iterations, writing tests could enhance development efficiency, but that’s based on the premise that function iterations are slow. If the function you just completed changes in a day or two, the related test code must be rewritten.

    So, it’s better not to write tests at all in these cases and rely on the testing team instead – because writing tests is very time-consuming and not worth the effort for these situations.

    Conclusion

    I’ve explained in detail how to write unit tests and E2E tests for NestJS projects. But I still want to reiterate the importance of testing. It can enhance the robustness, maintainability, and development efficiency of the system.

    If you don’t have the opportunity to write tests, I suggest you start a practice project yourself or participate in some open-source projects and contribute code to them. Open-source projects generally have stricter code requirements. Contributing code may require you to write new test cases or modify existing ones.

    Reference Materials

    • NestJS: A framework for building efficient, scalable Node.js server-side applications.

    • MongoDB: A NoSQL database used for data storage.

    • Jest: A testing framework for JavaScript and TypeScript.

    • Supertest: A library for testing HTTP servers.

    Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More 

    Facebook Twitter Reddit Email Copy Link
    Previous ArticleHow to Build RAG AI Agents with TypeScript
    Next Article How to Build a GraphQL API in Django

    Related Posts

    Repurposing Protein Folding Models for Generation with Latent Diffusion
    Artificial Intelligence

    Repurposing Protein Folding Models for Generation with Latent Diffusion

    September 14, 2025
    Artificial Intelligence

    Scaling Up Reinforcement Learning for Traffic Smoothing: A 100-AV Highway Deployment

    September 14, 2025
    Leave A Reply Cancel Reply

    For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

    Continue Reading

    Clair Obscur: Expedition 33 PC system requirements and specs — Can you run this turn-based RPG adventure?

    News & Updates

    CVE-2025-44897 – Fiberhome FW-WGS-804HPT Stack Overflow Vulnerability

    Common Vulnerabilities and Exposures (CVEs)

    canvural/larastan-strict-rules

    Development

    CVE-2025-45865 – TOTOLINK A3002R DNS Server Buffer Overflow Vulnerability

    Common Vulnerabilities and Exposures (CVEs)

    Highlights

    CVE-2024-54188 – Infoblox NETMRI File Disclosure

    May 22, 2025

    CVE ID : CVE-2024-54188

    Published : May 22, 2025, 3:16 p.m. | 1 hour, 31 minutes ago

    Description : Infoblox NETMRI before 7.6.1 has a vulnerability allowing remote authenticated users to read arbitrary files with root access.

    Severity: 0.0 | NA

    Visit the link for more details, such as CVSS details, affected products, timeline, and more…

    CVE-2024-7457 – Apple ws.stash.app macOS Privilege Escalation Vulnerability

    June 10, 2025

    Stardock Unveils Ashes of the Singularity 2: AI vs Humans in Epic Strategy Battles

    August 7, 2025

    CVE-2024-9639 – ASPECT Remote Code Execution Vulnerability

    May 22, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.