A test's structure should explain behavior, not hide it behind technical details. The two grouping tools - describe and test - carry meaning, not decoration. describe gathers tests around one unit or scenario and gives them a shared heading in the report; test describes one specific outcome. When the structure mirrors the system's behavior, the list of tests reads as its specification.
Inside a test you keep three phases - Arrange, Act, Assert - and separate them explicitly. Arrange prepares data via a builder or factory, Act performs one action under test, Assert compares the result with the expectation. Explicit phases let you read the test top to bottom without jumps: it is immediately clear what was prepared, what was called and what is checked. This matters most on a failure, when you need to see quickly where it diverged.
Test names are written in the language of behavior. 'applies 10% to a premium order' states both the condition and the outcome, while 'works' or 'test1' state nothing. A meaningful name disciplines the author: if the outcome is hard to name in one phrase, the test probably checks several things at once and it is time to split it.
The difference between a brittle and a durable test is what it is tied to. A brittle one reaches into a private detail, for example a private field via service['_discount'], and breaks on any rename, even with no change in behavior. A durable one checks the public contract - service.total(premiumOrder) - and survives an internals refactor as long as the outward promise did not change.
One test may contain several assertions if they all describe one outcome: for example, checking different fields of one result contract. Splitting such an object into five tests with the same Arrange is not worth it - that is noise without value. Conversely, five different scenarios in one test are five reasons to fail under one name, so different outcomes go into separate tests. The 'one assert per test' rule is a crude simplification: the guide is not one assertion but one outcome that those assertions together describe.
Nested describe blocks are handy for context: 'with an empty cart', 'for a premium user'. But nesting has a limit: when context rests on a chain of implicit beforeEach across three describe levels, the test stops reading locally, and you can only understand its state by assembling the whole hierarchy in your head. Explicit local setup beats a maze of implicit hooks.
Test files are placed predictably: next to the module (delivery.ts and delivery.test.ts) or in a neighboring tests. The file name mirrors the module name, so a red test in CI shows at once which piece of code is affected. A single convention matters more than the specific choice: the point is that a test's location follows from the code's location without thinking.
The typical failure is a structure that reflects the internals rather than behavior: tests laid out by class methods or implementation layers. Such a layout falls apart on a refactor and does not answer what the system promises the user. Start from scenarios and outcomes - then the structure survives a change of internal implementation and stays a readable specification.
describe('calculateDelivery', () => {
test('applies 10% to a premium order', () => {
const order = orderBuilder().withTotal(9_000).premium().build() // Arrange
const total = service.total(order) // Act
expect(total).toBe(8_100) // Assert
})
})// Brittle: tied to a private detail.
test('works', () => {
expect(service['_discount'](order)).toBe(0.1)
})
// Durable: the public contract.
test('applies 10% to a premium order', () => {
expect(service.total(premiumOrder)).toBe(8_100)
})