Skip to content

test-through-interface (BTTEST002)

Added in 0.13.0 · Preview · Off by default (warning)

Family: testing · Standard: CES-65 · Languages: javascript, python, typescript

This rule is unstable and in preview. It runs only with preview = true, and only when it is selected.

Directive

Tests call the unit under test through the public interface that production code calls, and assert on observable behaviour. A test reads no private attribute, calls no underscore-prefixed member and asserts on no internal state of the unit.

Why

A test bound to internals fails on each refactor that keeps the behaviour, and a test that fails with no change of behaviour gets deleted. A test through the interface passes after the refactor and checks the contract. When a behaviour cannot be seen without the internals, the interface is missing a query.

Flag

  • A read of a private attribute of the unit: service._store._rows.
  • A call of an underscore-prefixed method of the unit: parser._tokenize(text).
  • An assertion on an internal collection or counter that no public member exposes.

Do not flag

  • A private helper that the test file defines for itself.
  • A fixture or a fake that the test builds and inspects, such as an in-memory adapter.
  • A dunder method that is part of the protocol of the unit, such as __len__.

Confirm

Before reporting, check that the private member belongs to the unit under test and not to the test.

Do

result = service.register(CreateUser(email="a@b.com"))
assert service.find(result.id).email == "a@b.com"

Don't

service.register(CreateUser(email="a@b.com"))
assert service._store._rows[0]["email"] == "a@b.com"

Suppression

A file that has to read internals takes botica: ignore-file[test-through-interface] -- <reason> in a comment. A folder goes in the extend-ignores of the rule.

Discussion