Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/catalog/javascript-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ These mirror the [Python catalog](python.md) entries; the signal is the JavaScri
| C6 | LOW | weak check (`toBeTruthy`/`toBeDefined`, `.length > 0`) |
| C7 | HIGH | self-compare (`expect(x).toBe(x)`) |
| C8 | LOW | exact equality on a float |
| C8b | LOW | `toBeCloseTo` with no precision argument — the default 2-digit tolerance may be too loose |
| C9 | LOW | `toThrow()` with no error type or message |
| C11a | LOW | self-confirming literal — the expected value is bound from the same call under test (`const e = foo(); expect(foo()).toBe(e)`) |
| C16 | LOW | depends on `Date.now` / `new Date()` / `Math.random` / `crypto.randomUUID`/`getRandomValues` / a fixed timer |
| C18 | LOW | stringified equality (`String(x)` / `JSON.stringify` / template literal) |
| C20 | HIGH | dead assertion after `return` / `throw` / `process.exit` / an exhaustive `switch` (structured block-level reachability, cfg.ts) |
Expand Down Expand Up @@ -220,6 +222,88 @@ A Cypress query chain (`cy.get`/`cy.find`/`cy.contains`) used as a statement wit
never asserted, the cy.* analogue of JS13. Action commands (`click`/`type`/`visit`/...) do work
rather than query, so a chain ending in one stays clean, as does one ending in `.should`/`.and`.

### JS25 - the only assertion is inside an array-iterator callback
`J1` · HIGH · F2

The sole `expect` sits inside a `forEach` / `map` / `filter` / `some` / `every` / `flatMap`
callback. On an empty collection the callback never runs, so the test passes having asserted
nothing. Assert the length first, or pull at least one check outside the iterator.

=== "BAD"
```javascript
it('all valid', () => {
rows.forEach(r => expect(r.valid).toBe(true)); // JS25 - nothing runs if rows is []
});
```
=== "CLEAN"
```javascript
it('all valid', () => {
expect(rows.length).toBeGreaterThan(0);
rows.forEach(r => expect(r.valid).toBe(true));
});
```

### JS26 - fake timers installed but never advanced
`J1` · LOW · F2

`jest.useFakeTimers()` / `vi.useFakeTimers()` (or `sinon` fake timers) is set up, but the clock is
never advanced (`runAllTimers`, `advanceTimersByTime`, `tick`). The scheduled callback never fires,
so the assertion reads un-mutated state and passes for the wrong reason.

### JS27 - toHaveBeenCalled* is the sole oracle on a locally-created double
`J3` · LOW · F4

The only assertion is `toHaveBeenCalled` / `toHaveBeenCalledWith` / `toHaveBeenCalledTimes` on a
mock created in the test (`jest.fn()` / `vi.fn()`). It verifies the test's own wiring, that the
double was invoked, not that the unit produced the right result.
Comment on lines +256 to +258

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow JS27 to self-invoked mocks

When the locally-created mock is passed into the unit as a callback or dependency, toHaveBeenCalledWith / toHaveBeenCalledTimes can be the observable contract (for example, verifying that the unit calls a notifier with the right payload). Describing every sole call assertion on jest.fn() / vi.fn() as F4 will warn on legitimate interaction tests unless the rule is limited to mocks of the unit under test or doubles invoked only by the test itself.

Useful? React with 👍 / 👎.


### JS29 - resolves / rejects chain is a bare statement
`J1` · LOW · F2

`expect(p).resolves.toBe(...)` / `.rejects...` written as a statement that is neither `await`ed
nor `return`ed. The matcher returns a promise; the test finishes green before it settles, so a
later rejection is lost.

=== "BAD"
```javascript
it('resolves', () => {
expect(load()).resolves.toBe(42); // JS29 - not awaited or returned
});
```
=== "CLEAN"
```javascript
it('resolves', async () => {
await expect(load()).resolves.toBe(42);
});
```

### JS30 - literal-vs-literal assertion
`J2` · HIGH · F3

Both operands are fixed at parse time: `expect(2).toBe(3)`, chai `expect(1).to.equal(1)`. The
assertion does not touch the unit under test, so it is either always-true or always-false by
construction, never a check on real behaviour.
Comment on lines +283 to +285

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not classify failing literals as false-green

With the documented expect(2).toBe(3) case, Jest/Chai fail immediately, so that input is not a HIGH F3 false-green (the docs define F3 as always passes and HIGH as a definite false-green). Including always-false literal assertions in JS30 would make the scanner/docs report red tests as false-green and inflate the F3 coverage; restrict this code to provably true literal assertions or move unequal literals to diagnostics.

Useful? React with 👍 / 👎.


### JS31 - try/catch swallows a possible throw with no assertion on the exception
`J1` · LOW · F2

A `try` calls the unit and the `catch` neither re-throws nor asserts anything on the error. A unit
that stops throwing still passes green. Sibling of JS11, where the swallowed thing is an `expect`;
here there is no assertion at all on the caught path.

=== "BAD"
```javascript
it('throws on bad input', () => {
try { parse('bad'); } catch (e) { /* JS31 - swallowed, nothing asserted */ }
});
```
=== "CLEAN"
```javascript
it('throws on bad input', () => {
expect(() => parse('bad')).toThrow(SyntaxError);
});
```

## High-value traps with evidence

The scanner also catches a set of idiom-specific false-greens documented in the JS/TS empirical
Expand Down
84 changes: 84 additions & 0 deletions docs/catalog/javascript-typescript.pt.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ Estes espelham as entradas do [catálogo Python](python.md); o sinal é a forma
| C6 | BAIXO | verificação fraca (`toBeTruthy`/`toBeDefined`, `.length > 0`) |
| C7 | ALTO | autocomparação (`expect(x).toBe(x)`) |
| C8 | BAIXO | igualdade exata em um float |
| C8b | BAIXO | `toBeCloseTo` sem argumento de precisão — a tolerância padrão de 2 dígitos pode ser frouxa demais |
| C9 | BAIXO | `toThrow()` sem tipo de erro ou mensagem |
| C11a | BAIXO | literal autoconfirmante — o valor esperado é ligado da mesma chamada sob teste (`const e = foo(); expect(foo()).toBe(e)`) |
| C16 | BAIXO | depende de `Date.now` / `new Date()` / `Math.random` / `crypto.randomUUID`/`getRandomValues` / um timer fixo |
| C18 | BAIXO | igualdade de string (`String(x)` / `JSON.stringify` / template literal) |
| C20 | ALTO | asserção morta depois de `return` / `throw` / `process.exit` / um `switch` exaustivo (alcançabilidade estruturada no nível de bloco, cfg.ts) |
Expand Down Expand Up @@ -221,6 +223,88 @@ nunca é asserido, o análogo cy.* da JS13. Comandos de ação (`click`/`type`/`
trabalho em vez de consultar, então uma cadeia terminada num deles fica limpa, assim como uma
terminada em `.should`/`.and`.

### JS25 - a única asserção fica dentro de um callback de iterador de array
`J1` · ALTO · F2

A única `expect` fica dentro de um callback de `forEach` / `map` / `filter` / `some` / `every` /
`flatMap`. Numa coleção vazia o callback nunca roda, então o teste passa sem ter afirmado nada.
Afirme o comprimento antes, ou tire ao menos uma verificação para fora do iterador.

=== "RUIM"
```javascript
it('all valid', () => {
rows.forEach(r => expect(r.valid).toBe(true)); // JS25 - nada roda se rows for []
});
```
=== "LIMPO"
```javascript
it('all valid', () => {
expect(rows.length).toBeGreaterThan(0);
rows.forEach(r => expect(r.valid).toBe(true));
});
```

### JS26 - fake timers instalados mas nunca avançados
`J1` · BAIXO · F2

`jest.useFakeTimers()` / `vi.useFakeTimers()` (ou fake timers do `sinon`) é configurado, mas o
relógio nunca é avançado (`runAllTimers`, `advanceTimersByTime`, `tick`). O callback agendado nunca
dispara, então a asserção lê estado não-modificado e passa pelo motivo errado.

### JS27 - toHaveBeenCalled* é o único oráculo sobre um dublê criado localmente
`J3` · BAIXO · F4

A única asserção é `toHaveBeenCalled` / `toHaveBeenCalledWith` / `toHaveBeenCalledTimes` sobre um
mock criado no teste (`jest.fn()` / `vi.fn()`). Ela verifica a fiação do próprio teste, que o
dublê foi chamado, não que a unidade produziu o resultado certo.

### JS29 - cadeia resolves / rejects como instrução solta
`J1` · BAIXO · F2

`expect(p).resolves.toBe(...)` / `.rejects...` escrito como uma instrução que não é nem `await`ada
nem `return`ada. O matcher devolve uma promise; o teste termina verde antes de ela resolver, então
uma rejeição posterior se perde.

=== "RUIM"
```javascript
it('resolves', () => {
expect(load()).resolves.toBe(42); // JS29 - não aguardado nem retornado
});
```
=== "LIMPO"
```javascript
it('resolves', async () => {
await expect(load()).resolves.toBe(42);
});
```

### JS30 - asserção literal contra literal
`J2` · ALTO · F3

Ambos os operandos são fixos em tempo de parse: `expect(2).toBe(3)`, chai `expect(1).to.equal(1)`.
A asserção não toca a unidade sob teste, então é sempre-verdadeira ou sempre-falsa por construção,
nunca uma verificação do comportamento real.

### JS31 - try/catch engole um possível throw sem asserção sobre a exceção
`J1` · BAIXO · F2

Um `try` chama a unidade e o `catch` não relança nem afirma nada sobre o erro. Uma unidade que
para de lançar ainda passa verde. Irmã da JS11, onde o que é engolido é um `expect`; aqui não há
asserção nenhuma no caminho capturado.

=== "RUIM"
```javascript
it('throws on bad input', () => {
try { parse('bad'); } catch (e) { /* JS31 - engolido, nada afirmado */ }
});
```
=== "LIMPO"
```javascript
it('throws on bad input', () => {
expect(() => parse('bad')).toThrow(SyntaxError);
});
```

## Armadilhas de alto valor com evidência

O scanner também pega um conjunto de false-greens específicos de idioma documentados nos estudos
Expand Down
61 changes: 61 additions & 0 deletions docs/catalog/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,20 @@ confirms Python's attribute assignment works, not the production code.
assert product.price == 100 # C11a - just confirms assignment
```

### C52 - membership self-confirmation
`J2` · LOW · F3

`assert x in {x}` (or `x in [x]`, `x in (x,)`): the collection is built from the subject under
test, so membership holds by construction. A membership variant of C7. Checking against a
collection assembled independently of the subject is a real check.

=== "BAD"
```python
def test_tag():
tag = get_tag()
assert tag in {tag} # C52 - true by construction
```

### C13 - mock assertion misspelled or not called
`J4` · HIGH · F2

Expand Down Expand Up @@ -400,6 +414,13 @@ never runs, so the test may be checking a different line than intended.
sut.process(data) # C19 - intended target
```

### C49 - pytest.warns / assertWarns wraps more than one call
`J1` · LOW · F4

A `with pytest.warns(W):` / `assertWarns` / `deprecated_call()` block holds more than one
statement. An unrelated earlier line may emit the warning while the target never does, so the
test passes without exercising the warning under test. The warns sibling of C19.

### C28 - pytest.raises binding variable never read
`J4` · LOW · F4

Expand All @@ -418,12 +439,32 @@ checked but not its message or attributes.
assert "must be positive" in str(exc.value)
```

### C51 - empty-bodied pytest.raises / warns context
`J1` · HIGH · F1

`with pytest.raises(E):` (or `pytest.warns`) whose body is empty (`pass`, `...`, a comment).
No call is made inside the block, so the call that should raise never runs and the context
manager has nothing to catch. Always green.
Comment on lines +445 to +447

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove C51 from the false-green catalog

For the empty pytest.raises / pytest.warns body described here (pass, ..., or only a comment), pytest does not pass green: it fails with DID NOT RAISE or DID NOT WARN because no exception/warning was observed. Cataloging this as a HIGH F1 false-green will misclassify tests that are already red and also overstate coverage via the new C51 entry in the coverage table.

Useful? React with 👍 / 👎.


=== "BAD"
```python
with pytest.raises(ValueError):
pass # C51 - nothing called, nothing raised
```

### C29 - os.environ modified directly in a test
`J6` · LOW · F6

`os.environ["KEY"] = value`, `os.environ.update(...)`, or `os.putenv(...)` in a test body. The
change persists across tests in the same process. Use `monkeypatch.setenv()`.

### C55 - comparison between two mock-rooted values
`J3` · LOW · F4

`assert m.foo == m.bar` where both operands derive from the same test double (a `Mock`,
`MagicMock`, or a `patch`-injected object). Each side is the test's own configured value, so
the comparison checks the doubles against each other, not the SUT.

---

## Family D - the test depends on external or shared state
Expand Down Expand Up @@ -511,6 +552,26 @@ capture ran but nothing was checked.
assert out == "hello\n"
```

### C50 - caplog / assertLogs output captured but never asserted
`J4` · LOW · F1

`caplog` is read (`caplog.records`, `caplog.text`) or `self.assertLogs(...)` is entered, but the
captured output is never asserted: no comparison on the records, messages, or levels. The capture
ran and had no effect on pass/fail. The logging sibling of C31.
Comment on lines +558 to +560

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude assertLogs from passive capture checks

When this pattern is self.assertLogs(...), the context manager itself asserts that at least one log at the requested level is emitted; if none are emitted, unittest records a failure. Flagging every assertLogs block whose captured records are not inspected will report tests whose intended oracle is simply “logging happened” as F1/no-oracle, and the claim that the capture has no pass/fail effect is false.

Useful? React with 👍 / 👎.


=== "BAD"
```python
def test_logs(caplog):
run()
caplog.records # C50 - captured but never asserted
```
=== "CLEAN"
```python
def test_logs(caplog):
run()
assert "started" in caplog.text
```

### C32 - @pytest.mark.skip without reason
`J1` · LOW · F5

Expand Down
61 changes: 61 additions & 0 deletions docs/catalog/python.pt.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,20 @@ confirma que a atribuição de atributo do Python funciona, não o código de pr
assert product.price == 100 # C11a - só confirma a atribuição
```

### C52 - autoconfirmação de pertinência
`J2` · BAIXO · F3

`assert x in {x}` (ou `x in [x]`, `x in (x,)`): a coleção é construída a partir do próprio
sujeito sob teste, então a pertinência vale por construção. Uma variante de pertinência da C7.
Verificar contra uma coleção montada de forma independente do sujeito é uma verificação real.

=== "RUIM"
```python
def test_tag():
tag = get_tag()
assert tag in {tag} # C52 - verdadeira por construcao
```

### C13 - asserção de mock escrita errado ou não chamada
`J4` · ALTO · F2

Expand Down Expand Up @@ -400,6 +414,13 @@ nunca roda, então o teste pode estar verificando uma linha diferente da pretend
sut.process(data) # C19 - alvo pretendido
```

### C49 - pytest.warns / assertWarns envolve mais de uma chamada
`J1` · BAIXO · F4

Um bloco `with pytest.warns(W):` / `assertWarns` / `deprecated_call()` contém mais de uma
instrução. Uma linha anterior não relacionada pode emitir o warning enquanto o alvo nunca emite,
então o teste passa sem exercitar o warning sob teste. A irmã de warns da C19.

### C28 - variável de ligação do pytest.raises nunca lida
`J4` · BAIXO · F4

Expand All @@ -418,12 +439,32 @@ verificado mas não sua mensagem ou atributos.
assert "must be positive" in str(exc.value)
```

### C51 - contexto pytest.raises / warns de corpo vazio
`J1` · ALTO · F1

`with pytest.raises(E):` (ou `pytest.warns`) cujo corpo é vazio (`pass`, `...`, um comentário).
Nenhuma chamada é feita dentro do bloco, então a chamada que deveria lançar nunca roda e o
gerenciador de contexto não tem nada a capturar. Sempre verde.

=== "RUIM"
```python
with pytest.raises(ValueError):
pass # C51 - nada chamado, nada lançado
```

### C29 - os.environ modificado diretamente em um teste
`J6` · BAIXO · F6

`os.environ["KEY"] = value`, `os.environ.update(...)` ou `os.putenv(...)` no corpo de um teste. A
mudança persiste entre testes no mesmo processo. Use `monkeypatch.setenv()`.

### C55 - comparação entre dois valores enraizados em mock
`J3` · BAIXO · F4

`assert m.foo == m.bar` onde ambos os operandos derivam do mesmo dublê de teste (um `Mock`,
`MagicMock` ou um objeto injetado por `patch`). Cada lado é o próprio valor configurado pelo
teste, então a comparação verifica os dublês entre si, não o SUT.

---

## Família D - o teste depende de estado externo ou compartilhado
Expand Down Expand Up @@ -511,6 +552,26 @@ captura rodou mas nada foi verificado.
assert out == "hello\n"
```

### C50 - saída de caplog / assertLogs capturada mas nunca afirmada
`J4` · BAIXO · F1

`caplog` é lido (`caplog.records`, `caplog.text`) ou `self.assertLogs(...)` é aberto, mas a saída
capturada nunca é afirmada: nenhuma comparação sobre os registros, mensagens ou níveis. A captura
rodou e não teve efeito no pass/fail. A irmã de logging da C31.

=== "RUIM"
```python
def test_logs(caplog):
run()
caplog.records # C50 - capturado mas nunca afirmado
```
=== "LIMPO"
```python
def test_logs(caplog):
run()
assert "started" in caplog.text
```

### C32 - @pytest.mark.skip sem reason
`J1` · BAIXO · F5

Expand Down
Loading
Loading