Scenario and definition
A Banking Application's FundsTransferService needs unit tests verifying it correctly rejects insufficient-balance transfers without hitting a real database, plus an integration test verifying the full HTTP endpoint correctly persists a transfer end-to-end. Node.js (NestJS) uses Jest combined with NestJS's TestingModule for dependency mocking. Spring Boot uses JUnit 5 combined with Mockito for unit tests and @SpringBootTest for full-context integration tests.
Keywords
Code comparison
describe('FundsTransferService', () => {
let service: FundsTransferService;
let accountRepo: jest.Mocked<AccountRepository>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
FundsTransferService,
{ provide: AccountRepository, useValue: { lockForUpdate: jest.fn(), debit: jest.fn(), credit: jest.fn() } },
{ provide: FraudCheckService, useValue: { isSuspicious: jest.fn().mockResolvedValue(false) } }
]
}).compile();
service = module.get(FundsTransferService);
accountRepo = module.get(AccountRepository);
});
it('rejects transfer when balance is insufficient', async () => {
accountRepo.lockForUpdate.mockResolvedValue({ id: 'acc1', balance: 50 });
await expect(service.transfer('acc1', 'acc2', 100)).rejects.toThrow(BadRequestException);
});
});@ExtendWith(MockitoExtension.class)
class FundsTransferServiceTest {
@Mock private AccountRepository accountRepo;
@Mock private FraudCheckService fraudCheck;
@InjectMocks private FundsTransferService service;
@Test
void rejectsTransferWhenBalanceIsInsufficient() {
when(fraudCheck.isSuspicious(any(), any())).thenReturn(false);
when(accountRepo.findByIdForUpdate("acc1")).thenReturn(new Account("acc1", new BigDecimal("50")));
assertThrows(InsufficientBalanceException.class, () ->
service.transfer("acc1", "acc2", new BigDecimal("100")));
}
}Code explanation
node
Test.createTestingModule replicates NestJS's real dependency injection container in a test context, allowing real production providers to be selectively overridden with mock objects — the test exercises the EXACT same DI resolution path the real application uses. jest.fn() creates mock functions whose return values are configured per-test, and .rejects.toThrow() asserts the returned promise rejects with the expected exception type.
springboot
@Mock combined with @InjectMocks is Mockito's lighter-weight unit testing pattern — unlike NestJS's TestingModule, this does NOT spin up any part of Spring's actual DI container; @InjectMocks constructs a real FundsTransferService instance and manually injects the @Mock-annotated fields via reflection, faster and more isolated but not verifying Spring's own wiring correctness.
Backend integration
node
The unit test mocks AccountRepository entirely, never touching a real database; a separate integration test suite typically uses supertest against a running NestJS app connected to a dedicated test database, often via Testcontainers.
springboot
Identically, the Mockito unit test never touches a database; @SpringBootTest-based integration tests commonly pair with Testcontainers, with particularly mature Spring Boot integration via @Testcontainers and @DynamicPropertySource.
Interview questions
What's the meaningful difference between NestJS's TestingModule-based unit test and Spring's @Mock/@InjectMocks-based unit test in terms of what each actually verifies?
NestJS's Test.createTestingModule constructs an actual scaled-down instance of Nest's real DI container, implicitly verifying that FundsTransferService's dependencies are correctly resolvable — a misconfigured provider token would surface as a test failure even in a 'unit' test. Spring's @Mock/@InjectMocks deliberately bypasses Spring's ApplicationContext entirely, meaning a broken @Qualifier or missing bean definition would NOT be caught by this style of test, only by a slower @SpringBootTest.
Why would a team use Testcontainers rather than mocking the database entirely or pointing tests at a shared, persistent test database?
Mocking the database entirely verifies business logic correctness but says nothing about whether the actual generated SQL or real database constraints work correctly. Pointing tests at a shared, persistent test database introduces test pollution risk and makes tests non-reproducible in parallel CI; Testcontainers spins up a fresh, ephemeral, real database instance per test run, giving full fidelity while remaining completely isolated.