A good test solves two problems at once: it checks behavior and explains it to whoever opens the file six months later. Hence the language of structure. describe groups tests of one element of the system; test (also spelled it) describes one scenario; the body of the test falls into three phases - Arrange, Act, Assert, that is preparing the data, calling the code under test, and checking the result. In the running example this is the calculateDelivery function: it computes the shipping cost for an order, and its behavior we want to pin down so that the test reads like a specification.
A natural but deceptive habit is to write tests any old way. A name like test('works'), one assertion per test by the dogma 'one assert - one test', reaching into the service's private fields to 'check everything'. But such a test explains nothing: the name says nothing about behavior, the splitting scatters one outcome across five files, and binding to internals makes the suite brittle.
Start with the phases. Arrange prepares the input - here you assemble the order through a builder so the intent is visible. Act is exactly one call of the code under test, with no logic around it. Assert checks the outcome. Splitting into three parts is not decoration: it shows the reader what exactly is the input, what is the action, and what is the checked result, and it instantly exposes a test where there are several actions or the check is mixed into the setup.
Now the names. describe carries the element's name - calculateDelivery, while test states behavior in the language of the domain, not of the implementation. 'Makes delivery free from 5,000 rubles' is a claim about a business rule that even a non-programmer understands. Such a name survives refactoring: as long as the rule holds, the test is meaningful, however the internals of the function change.
One test may have several assertions if they describe a single outcome. This directly contradicts the 'one assert per test' dogma, and rightly so: if the result of calculateDelivery is an object with a price, a term, and a flag, checking all three fields in one test is more honest than splitting into three tests with identical Arrange. Breaking one contract into five tests with identical setup adds no rigor - only noise.
Separately, the temptation to check the private. A test reaching into service['_discount'] binds itself to implementation details that are not a contract. It reports only that a private method returned a number, but not about the observable behavior of the service. Rename or inline that method during a refactor, and the green test turns red although the system's behavior did not change. This is a false signal, and false signals cost more than a missing test.
test('works', () => {
expect(service['_discount'](x)).toBe(10)
})Compare it with a check through the public boundary. The same meaning - a discount for a premium order - is expressed through the observable result: total(premiumOrder) equals 900. Here behavior is checked, not mechanism; the implementation can be rewritten however you like as long as the public total stays correct. The test name remains a claim about a rule, not about a private method.
test('applies 10% to a premium order', () => {
expect(service.total(premiumOrder)).toBe(900)
})The price of structure is small - a few more words in the name and the discipline of three phases - while the payoff is that the suite reads like a living specification and does not break on refactoring. There is the opposite skew too - excessive splitting and chasing DRY at the expense of readability: in tests clarity matters more than removing duplication, a little repetition for the sake of clarity is fine. A typical failure of poor structure: a developer renamed a private method, half the suite went red, and half a day went into fixing tests that checked the implementation instead of the behavior.
describe('calculateDelivery', () => {
test('makes delivery free from 5,000 RUB', () => {
const order = orderBuilder().withTotal(5_000).build() // Arrange
const price = calculateDelivery(order) // Act
expect(price).toBe(0) // Assert
})
})