Lesson 54 of 5820 min read

NestJS Integration Testing Tutorial Step by Step

Learn how integration tests differ from unit tests in NestJS, verifying multiple real components working together, such as a service with a real test database.

Author: CodersNexus

NestJS Integration Testing Tutorial Step by Step

Unit tests, covered in the previous two lessons, verify a single class's logic in isolation, mocking away every dependency. But some bugs only surface when real components actually interact, a repository's query genuinely hitting a database, a module's providers actually being wired together correctly. Integration tests occupy this middle layer of the testing pyramid, verifying that multiple real pieces work correctly together.

NestJS Integration Testing Tutorial: Learning Objectives

  • Understand what distinguishes an integration test from a unit test and an e2e test.
  • Set up a NestJS integration test using a real, but isolated, test database.
  • Write an integration test verifying a service's actual interaction with a real repository.
  • Understand the trade-offs of integration tests: more realistic, but slower than unit tests.
  • Recognize which parts of an application benefit most from integration-level testing.

NestJS Integration Testing Tutorial: Key Terms and Definitions

  • Integration test: A test verifying that multiple real components, such as a service and a real (test) database, work correctly together, sitting between unit and e2e tests in scope.
  • Test database: A separate, isolated database instance (often SQLite in-memory, or a dedicated test schema) used exclusively during test runs, never touching real production or development data.
  • TypeOrmModule.forRoot() (test configuration): The same dynamic module from Module 3, here configured to point at a test database instead of a real one.
  • Test isolation: Ensuring tests don't interfere with each other or leave persistent side effects, often achieved by resetting or recreating the test database between test runs.
  • afterEach() / afterAll(): Jest functions for cleanup logic run after each test or after all tests in a describe block, commonly used to clear test database state.

How NestJS Integration Testing Tutorial Works: Detailed Explanation

While a unit test for UsersService mocks its repository entirely, an integration test instead connects UsersService to a real TypeORM repository, backed by a real, but isolated, test database, verifying that the service's queries actually produce correct results against genuine database behavior, constraints, and relationships that a mock could never fully replicate. This catches an entirely different category of bugs: a subtly incorrect TypeORM query, an unexpected interaction with a database constraint, or a relationship that isn't configured quite right, issues invisible to a unit test where the repository was mocked away entirely.

Setting up an integration test typically still uses Test.createTestingModule(), but instead of mocking the repository, you import a real TypeOrmModule.forRoot() configuration pointed at a dedicated test database, commonly SQLite running entirely in-memory for speed and to avoid any external database server dependency during test runs, or a separate, disposable test schema on a real database server in more complex setups. The service and its real repository are both included as genuine providers, letting NestJS's actual dependency injection wire them together exactly as it would in production, just against test data instead of real data.

A critical concern specific to integration tests is test isolation and cleanup: since these tests actually write to a real (test) database, one test's data could interfere with another test's assertions if not properly cleaned up between runs. Using Jest's afterEach() or afterAll() hooks to clear relevant tables, or configuring an in-memory SQLite database that's recreated fresh for each test suite, ensures each test starts from a known, clean state, avoiding subtle, hard-to-diagnose failures caused by leftover data from a previous test.

The trade-off compared to unit tests is clear: integration tests are meaningfully slower, since they involve real (even if in-memory) database operations, and setup is more involved. This is exactly why the testing pyramid recommends fewer integration tests than unit tests, reserving them specifically for verifying genuine cross-component interactions, like a service's actual database queries, relationships, or transaction behavior, rather than testing every possible business logic branch, which unit tests with mocks already cover far more cheaply and quickly.

Interview-Friendly Explanation

A strong interview or viva answer for nestjs integration testing tutorial must go beyond a one-line definition. Interviewers evaluating backend developers reward answers that combine a precise definition, a clear working explanation, a real-world example, and a conclusion that highlights why it matters for shipping reliable production Node.js applications. Use the four-point framework below when answering under time pressure.

  • Definition point: Integration test: A test verifying that multiple real components, such as a service and a real (test) database, work correctly together, sitting between unit and e2e tests in scope.
  • Working point: Set up a NestJS integration test using a real, but isolated, test database.
  • Example point: Applications with complex TypeORM relationships (from Module 3) often specifically rely on integration tests to verify that joins, cascades, and foreign key constraints behave correctly, since these interactions are difficult to meaningfully mock.
  • Conclusion point: Integration tests catch a genuinely different category of bugs than mocked unit tests, ones involving real database or cross-component behavior.

How to Answer This in a Technical Interview

  1. Give a two-to-three sentence definition using correct testing terminology (unit, integration, e2e).
  2. Add one specific example drawn from a real backend scenario such as testing an auth flow or a CRUD API.
  3. Mention a relevant trade-off, such as speed versus realism, since interviewers often probe this contrast.
  4. Close with one benefit, limitation, or production consequence of getting this wrong.
  5. Avoid vague answers like "it just makes sure the code works" — interviewers filter these out immediately.

Practical Scenario

Imagine you are shipping frequent changes to a production backend for a SaaS product, similar to systems used at companies like Razorpay, Freshworks, or Zomato. Every lesson in this module maps directly to a decision you will make to ship confidently: what to test, how to isolate it, and how to know your test suite is actually catching real problems before they reach users.

NestJS Integration Testing Tutorial: Architecture and Flow Diagram

Visualize where integration tests sit compared to unit and e2e tests:

[Unit Test] --> UsersService with a MOCKED repository --> Tests UsersService's own logic only
[Integration Test] --> UsersService with a REAL repository + REAL (test) database --> Tests UsersService AND its actual database interaction together
[E2E Test] --> Full running application, real HTTP requests --> Tests the entire user-facing flow end to end

What's Real vs What's Mocked/Isolated vs Speed: Comparison Table

Test TypeWhat's RealWhat's Mocked/IsolatedSpeed
Unit testThe class under test onlyAll its dependencies (repositories, other services)Fastest
Integration testThe class under test + its real dependencies (e.g. a test database)External systems outside this specific integration (e.g. third-party APIs)Moderate
E2E testThe entire running application, real HTTP requestsNothing internal; tests the whole stack togetherSlowest

NestJS Integration Testing Tutorial: NestJS Code Example

// npm install --save-dev sqlite3

// users.service.integration.spec.ts — testing UsersService against a REAL test database
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { User } from './user.entity';

describe('UsersService (integration)', () => {
  let service: UsersService;
  let module: TestingModule;

  beforeAll(async () => {
    module = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot({
          type: 'sqlite',
          database: ':memory:', // a fresh, isolated in-memory database
          entities: [User],
          synchronize: true, // fine here since it's a disposable test database
        }),
        TypeOrmModule.forFeature([User]),
      ],
      providers: [UsersService],
    }).compile();

    service = module.get<UsersService>(UsersService);
  });

  afterEach(async () => {
    // Clean up between tests so one test's data doesn't affect another
    const repository = module.get('UserRepository');
    await repository.clear();
  });

  afterAll(async () => {
    await module.close(); // properly closes the in-memory database connection
  });

  it('should create and then retrieve a real user from the test database', async () => {
    const created = await service.create({ name: 'Asha Mehta', email: 'asha@example.com' });

    const found = await service.findOne(created.id);

    expect(found).toBeDefined();
    expect(found.email).toBe('asha@example.com');
  });

  it('should throw NotFoundException for a genuinely non-existent user', async () => {
    await expect(service.findOne(99999)).rejects.toThrow();
  });
});

This integration test imports a real TypeOrmModule.forRoot() configuration pointed at an in-memory SQLite database (database: ':memory:'), with synchronize: true acceptable here specifically because this database is entirely disposable and recreated fresh for the test run, unlike the production danger this same setting poses covered in Module 3. UsersService and its real repository (via TypeOrmModule.forFeature([User])) are both genuine, non-mocked providers, meaning calling service.create() and service.findOne() actually performs real database operations against this test database. The afterEach() hook clears the repository's data after every test, preventing one test's created user from affecting another test's assertions, while afterAll() properly closes the database connection once the entire suite finishes. The first test verifies a genuinely realistic create-then-retrieve flow works correctly against real database behavior, something a fully mocked unit test could not verify with the same confidence.

Real-World NestJS Integration Testing Tutorial Industry Examples

  • Applications with complex TypeORM relationships (from Module 3) often specifically rely on integration tests to verify that joins, cascades, and foreign key constraints behave correctly, since these interactions are difficult to meaningfully mock.
  • Teams building financial or inventory systems, where subtle database-level bugs can have serious real-world consequences, frequently invest more heavily in integration tests around their core transactional logic than the general testing pyramid guideline might otherwise suggest.
  • CI pipelines commonly spin up a genuinely separate, ephemeral test database (or use in-memory SQLite, as in this lesson) specifically to run integration tests safely and repeatably without any risk to real development or production data.
  • Migration-heavy applications sometimes use integration tests to verify that a set of TypeORM migrations, once applied to a fresh test database, actually produce a schema that the application's real queries can successfully operate against.

NestJS Integration Testing Tutorial Interview Questions and Answers

Q1. What distinguishes an integration test from a unit test in NestJS?

Short answer: A unit test isolates a single class by mocking all of its dependencies, testing only that class's own logic. An integration test instead uses real dependencies, such as a genuine (though isolated, test-specific) database connection, verifying that multiple real components, like a service and its actual repository, correctly work together, catching a different category of bugs that mocking would hide.

Detailed explanation: While a unit test for UsersService mocks its repository entirely, an integration test instead connects UsersService to a real TypeORM repository, backed by a real, but isolated, test database, verifying that the service's queries actually produce correct results against genuine database behavior, constraints, and relationships that a mock could never fully replicate. This catches an entirely different category of bugs: a subtly incorrect TypeORM query, an unexpected interaction with a database constraint, or a relationship that isn't configured quite right, issues invisible to a unit test where the repository was mocked away entirely. Setting up an integration test typically still uses Test.createTestingModule(), but instead of mocking the repository, you import a real TypeOrmModule.forRoot() configuration pointed at a dedicated test database, commonly SQLite running entirely in-memory for speed and to avoid any external database server dependency during test runs, or a separate, disposable test schema on a real database server in more complex setups. The service and its real repository are both included as genuine providers, letting NestJS's actual dependency injection wire them together exactly as it would in production, just against test data instead of real data. A critical concern specific to integration tests is test isolation and cleanup: since these tests actually write to a real (test) database, one test's data could interfere with another test's assertions if not properly cleaned up between runs. Using Jest's afterEach() or afterAll() hooks to clear relevant tables, or configuring an in-memory SQLite database that's recreated fresh for each test suite, ensures each test starts from a known, clean state, avoiding subtle, hard-to-diagnose failures caused by leftover data from a previous test. The trade-off compared to unit tests is clear: integration tests are meaningfully slower, since they involve real (even if in-memory) database operations, and setup is more involved. This is exactly why the testing pyramid recommends fewer integration tests than unit tests, reserving them specifically for verifying genuine cross-component interactions, like a service's actual database queries, relationships, or transaction behavior, rather than testing every possible business logic branch, which unit tests with mocks already cover far more cheaply and quickly.

Practical example: Applications with complex TypeORM relationships (from Module 3) often specifically rely on integration tests to verify that joins, cascades, and foreign key constraints behave correctly, since these interactions are difficult to meaningfully mock.

Interview tip: Integration tests verify real components (e.g. a service + a real test database) working together, unlike mocked unit tests.

Revision hook: Integration tests catch a genuinely different category of bugs than mocked unit tests, ones involving real database or cross-component behavior.

Q2. Why might a team choose to use an in-memory SQLite database for integration tests instead of a real production-like database?

Short answer: An in-memory SQLite database is extremely fast to create and tear down, requires no external database server to be running, and is entirely disposable, making it ideal for integration tests that need real database behavior without the overhead, setup complexity, or shared-state risk of connecting to an actual persistent database server.

Detailed explanation: This integration test imports a real TypeOrmModule.forRoot() configuration pointed at an in-memory SQLite database (database: ':memory:'), with synchronize: true acceptable here specifically because this database is entirely disposable and recreated fresh for the test run, unlike the production danger this same setting poses covered in Module 3. UsersService and its real repository (via TypeOrmModule.forFeature([User])) are both genuine, non-mocked providers, meaning calling service.create() and service.findOne() actually performs real database operations against this test database. The afterEach() hook clears the repository's data after every test, preventing one test's created user from affecting another test's assertions, while afterAll() properly closes the database connection once the entire suite finishes. The first test verifies a genuinely realistic create-then-retrieve flow works correctly against real database behavior, something a fully mocked unit test could not verify with the same confidence.

Practical example: Teams building financial or inventory systems, where subtle database-level bugs can have serious real-world consequences, frequently invest more heavily in integration tests around their core transactional logic than the general testing pyramid guideline might otherwise suggest.

Interview tip: In-memory SQLite (or a dedicated disposable test database) is a common, fast choice for NestJS integration tests.

Revision hook: An in-memory SQLite database is a fast, practical way to get real database behavior in tests without external infrastructure dependencies.

Q3. Why is test cleanup, like using afterEach() to clear data, particularly important in integration tests?

Short answer: Since integration tests perform real writes to a (test) database, data created by one test could persist and interfere with a subsequent test's assertions if not cleaned up, potentially causing confusing, order-dependent test failures; explicit cleanup after each test ensures every test starts from a known, consistent state.

Detailed explanation: Integration tests verify that multiple real components genuinely work correctly together, such as a NestJS service connected to an actual (though isolated, test-specific) database, catching bugs like incorrect queries or relationship misconfigurations that a fully mocked unit test would never surface. Setting one up still uses Test.createTestingModule(), but imports a real TypeOrmModule.forRoot() configuration pointed at a disposable test database, commonly an in-memory SQLite instance for speed and simplicity, letting the service and its real repository be genuinely wired together by NestJS's actual dependency injection. Because these tests perform real writes, explicit cleanup using Jest's afterEach() and afterAll() hooks is essential to prevent one test's data from interfering with another's assertions. Sitting in the middle of the testing pyramid, integration tests are more realistic than mocked unit tests but slower to run, making them best reserved for verifying genuine cross-component behavior rather than covering every business logic branch.

Practical example: CI pipelines commonly spin up a genuinely separate, ephemeral test database (or use in-memory SQLite, as in this lesson) specifically to run integration tests safely and repeatably without any risk to real development or production data.

Interview tip: afterEach()/afterAll() cleanup is essential to prevent test data from leaking between tests and causing flaky failures.

Revision hook: Proper test cleanup between runs is non-negotiable for integration tests, given they perform real, persistent writes.

Q4. Why does the testing pyramid recommend fewer integration tests than unit tests?

Short answer: Integration tests are meaningfully slower and more complex to set up than unit tests, since they involve real (even if in-memory) database operations and more elaborate module configuration, making them best reserved for verifying genuine cross-component interactions rather than every possible business logic branch, which mocked unit tests can cover far more cheaply.

Detailed explanation: While a unit test for UsersService mocks its repository entirely, an integration test instead connects UsersService to a real TypeORM repository, backed by a real, but isolated, test database, verifying that the service's queries actually produce correct results against genuine database behavior, constraints, and relationships that a mock could never fully replicate. This catches an entirely different category of bugs: a subtly incorrect TypeORM query, an unexpected interaction with a database constraint, or a relationship that isn't configured quite right, issues invisible to a unit test where the repository was mocked away entirely. Setting up an integration test typically still uses Test.createTestingModule(), but instead of mocking the repository, you import a real TypeOrmModule.forRoot() configuration pointed at a dedicated test database, commonly SQLite running entirely in-memory for speed and to avoid any external database server dependency during test runs, or a separate, disposable test schema on a real database server in more complex setups. The service and its real repository are both included as genuine providers, letting NestJS's actual dependency injection wire them together exactly as it would in production, just against test data instead of real data. A critical concern specific to integration tests is test isolation and cleanup: since these tests actually write to a real (test) database, one test's data could interfere with another test's assertions if not properly cleaned up between runs. Using Jest's afterEach() or afterAll() hooks to clear relevant tables, or configuring an in-memory SQLite database that's recreated fresh for each test suite, ensures each test starts from a known, clean state, avoiding subtle, hard-to-diagnose failures caused by leftover data from a previous test. The trade-off compared to unit tests is clear: integration tests are meaningfully slower, since they involve real (even if in-memory) database operations, and setup is more involved. This is exactly why the testing pyramid recommends fewer integration tests than unit tests, reserving them specifically for verifying genuine cross-component interactions, like a service's actual database queries, relationships, or transaction behavior, rather than testing every possible business logic branch, which unit tests with mocks already cover far more cheaply and quickly.

Practical example: Migration-heavy applications sometimes use integration tests to verify that a set of TypeORM migrations, once applied to a fresh test database, actually produce a schema that the application's real queries can successfully operate against.

Interview tip: Integration tests sit in the pyramid's middle layer: more realistic than unit tests, faster and narrower in scope than e2e tests.

Revision hook: Reserve integration tests for genuine cross-component concerns; let cheaper, faster unit tests handle business logic branching.

NestJS Integration Testing Tutorial MCQs and Practice Questions

1. What is the key difference between an integration test and a unit test?

  1. Integration tests never use Jest
  2. Integration tests use real dependencies (like a test database) instead of mocking them
  3. Integration tests are always faster than unit tests
  4. Integration tests don't use Test.createTestingModule()

Answer: B. Integration tests use real dependencies (like a test database) instead of mocking them

Explanation: Unlike unit tests, which mock away dependencies to isolate a single class, integration tests use real (though isolated, test-specific) dependencies to verify multiple components genuinely work together.

Concept link: Integration tests verify real components (e.g. a service + a real test database) working together, unlike mocked unit tests.

Why this matters: Integration tests catch a genuinely different category of bugs than mocked unit tests, ones involving real database or cross-component behavior.

2. Why is an in-memory SQLite database commonly used for NestJS integration tests?

  1. It's required by Jest
  2. It's fast, requires no external server, and is fully disposable between test runs
  3. It's the only database TypeORM supports
  4. It automatically mocks all queries

Answer: B. It's fast, requires no external server, and is fully disposable between test runs

Explanation: An in-memory SQLite database provides genuine database behavior for integration tests without the overhead of running and managing a separate, persistent database server.

Concept link: In-memory SQLite (or a dedicated disposable test database) is a common, fast choice for NestJS integration tests.

Why this matters: An in-memory SQLite database is a fast, practical way to get real database behavior in tests without external infrastructure dependencies.

3. What is the purpose of an afterEach() cleanup hook in an integration test?

  1. To skip failing tests
  2. To clear database state so one test's data doesn't affect a subsequent test
  3. To speed up test execution
  4. To mock the database automatically

Answer: B. To clear database state so one test's data doesn't affect a subsequent test

Explanation: Since integration tests write real data to a test database, afterEach() cleanup ensures each test starts from a clean, consistent state, preventing leftover data from causing unreliable, order-dependent test failures.

Concept link: afterEach()/afterAll() cleanup is essential to prevent test data from leaking between tests and causing flaky failures.

Why this matters: Proper test cleanup between runs is non-negotiable for integration tests, given they perform real, persistent writes.

4. Where do integration tests sit within the testing pyramid relative to unit and e2e tests?

  1. Below unit tests, forming the widest base
  2. In the middle, between unit tests and e2e tests
  3. Above e2e tests, at the very top
  4. Integration tests are not part of the testing pyramid

Answer: B. In the middle, between unit tests and e2e tests

Explanation: Integration tests occupy the middle layer of the testing pyramid, being more realistic than mocked unit tests but faster and less comprehensive than full end-to-end tests of the entire running application.

Concept link: Integration tests sit in the pyramid's middle layer: more realistic than unit tests, faster and narrower in scope than e2e tests.

Why this matters: Reserve integration tests for genuine cross-component concerns; let cheaper, faster unit tests handle business logic branching.

Common NestJS Integration Testing Tutorial Mistakes to Avoid

  • Forgetting to clean up test database state between tests, causing confusing, order-dependent failures caused by leftover data.
  • Using synchronize: true against a real, shared database in integration tests (rather than a genuinely disposable in-memory or dedicated test database), risking unintended schema changes.
  • Writing too many integration tests covering business logic that mocked unit tests could verify far more quickly and cheaply.
  • Forgetting to call module.close() after integration tests complete, potentially leaving lingering open database connections.

NestJS Integration Testing Tutorial: Interview Notes and Exam Tips

  • Integration tests verify real components (e.g. a service + a real test database) working together, unlike mocked unit tests.
  • In-memory SQLite (or a dedicated disposable test database) is a common, fast choice for NestJS integration tests.
  • afterEach()/afterAll() cleanup is essential to prevent test data from leaking between tests and causing flaky failures.
  • Integration tests sit in the pyramid's middle layer: more realistic than unit tests, faster and narrower in scope than e2e tests.

Key NestJS Integration Testing Tutorial Takeaways

  • Integration tests catch a genuinely different category of bugs than mocked unit tests, ones involving real database or cross-component behavior.
  • An in-memory SQLite database is a fast, practical way to get real database behavior in tests without external infrastructure dependencies.
  • Proper test cleanup between runs is non-negotiable for integration tests, given they perform real, persistent writes.
  • Reserve integration tests for genuine cross-component concerns; let cheaper, faster unit tests handle business logic branching.

NestJS Integration Testing Tutorial: Summary

Integration tests verify that multiple real components genuinely work correctly together, such as a NestJS service connected to an actual (though isolated, test-specific) database, catching bugs like incorrect queries or relationship misconfigurations that a fully mocked unit test would never surface. Setting one up still uses Test.createTestingModule(), but imports a real TypeOrmModule.forRoot() configuration pointed at a disposable test database, commonly an in-memory SQLite instance for speed and simplicity, letting the service and its real repository be genuinely wired together by NestJS's actual dependency injection. Because these tests perform real writes, explicit cleanup using Jest's afterEach() and afterAll() hooks is essential to prevent one test's data from interfering with another's assertions. Sitting in the middle of the testing pyramid, integration tests are more realistic than mocked unit tests but slower to run, making them best reserved for verifying genuine cross-component behavior rather than covering every business logic branch.

Frequently Asked Questions

Not necessarily; an in-memory SQLite database, as shown in this lesson, provides genuine database behavior without requiring any external, persistent database server, making it a fast and practical default choice for most NestJS integration tests, though some teams do use a dedicated test schema on a real database server for closer parity with production. In interviews, tie this back to: Integration tests verify real components (e.g. a service + a real test database) working together, unlike mocked unit tests. In real applications, consider this example: Applications with complex TypeORM relationships (from Module 3) often specifically rely on integration tests to verify that joins, cascades, and foreign key constraints behave correctly, since these interactions are difficult to meaningfully mock. Key revision takeaway: Integration tests catch a genuinely different category of bugs than mocked unit tests, ones involving real database or cross-component behavior.

Following the testing pyramid's guidance, integration tests should be noticeably fewer than unit tests, reserved specifically for verifying genuine cross-component behavior like database interactions or module wiring, while the much larger volume of business logic branches are better covered by faster, cheaper unit tests. In interviews, tie this back to: In-memory SQLite (or a dedicated disposable test database) is a common, fast choice for NestJS integration tests. In real applications, consider this example: Teams building financial or inventory systems, where subtle database-level bugs can have serious real-world consequences, frequently invest more heavily in integration tests around their core transactional logic than the general testing pyramid guideline might otherwise suggest. Key revision takeaway: An in-memory SQLite database is a fast, practical way to get real database behavior in tests without external infrastructure dependencies.

Data created by one test may persist and interfere with a subsequent test's assertions, causing failures that depend on test execution order or seem to appear and disappear inconsistently, which is exactly the kind of flaky, hard-to-diagnose behavior proper afterEach() or afterAll() cleanup is meant to prevent. In interviews, tie this back to: afterEach()/afterAll() cleanup is essential to prevent test data from leaking between tests and causing flaky failures. In real applications, consider this example: CI pipelines commonly spin up a genuinely separate, ephemeral test database (or use in-memory SQLite, as in this lesson) specifically to run integration tests safely and repeatably without any risk to real development or production data. Key revision takeaway: Proper test cleanup between runs is non-negotiable for integration tests, given they perform real, persistent writes.

Yes, unlike in a real production environment (covered in Module 3), using synchronize: true is generally acceptable and convenient for a genuinely disposable, isolated test database like an in-memory SQLite instance, since there's no risk of it affecting real, persistent data. In interviews, tie this back to: Integration tests sit in the pyramid's middle layer: more realistic than unit tests, faster and narrower in scope than e2e tests. In real applications, consider this example: Migration-heavy applications sometimes use integration tests to verify that a set of TypeORM migrations, once applied to a fresh test database, actually produce a schema that the application's real queries can successfully operate against. Key revision takeaway: Reserve integration tests for genuine cross-component concerns; let cheaper, faster unit tests handle business logic branching.

Yes, an integration test could include a controller alongside its real service and repository dependencies in the testing module, testing the controller's interaction with genuinely working business logic and data, though this starts to overlap conceptually with what e2e tests, covered in the next lesson, more fully verify through actual HTTP requests. In interviews, tie this back to: Integration tests verify real components (e.g. a service + a real test database) working together, unlike mocked unit tests. In real applications, consider this example: Applications with complex TypeORM relationships (from Module 3) often specifically rely on integration tests to verify that joins, cascades, and foreign key constraints behave correctly, since these interactions are difficult to meaningfully mock. Key revision takeaway: Integration tests catch a genuinely different category of bugs than mocked unit tests, ones involving real database or cross-component behavior.

While more realistic, integration tests are slower and more complex to set up and maintain than unit tests, and relying on them for every possible test case would make the overall test suite unnecessarily slow and expensive to run, which is exactly why the testing pyramid recommends reserving them for genuine cross-component verification rather than exhaustive logic coverage. In interviews, tie this back to: In-memory SQLite (or a dedicated disposable test database) is a common, fast choice for NestJS integration tests. In real applications, consider this example: Teams building financial or inventory systems, where subtle database-level bugs can have serious real-world consequences, frequently invest more heavily in integration tests around their core transactional logic than the general testing pyramid guideline might otherwise suggest. Key revision takeaway: An in-memory SQLite database is a fast, practical way to get real database behavior in tests without external infrastructure dependencies.