diff --git a/docs/catalog/javascript-typescript.md b/docs/catalog/javascript-typescript.md index 20ed882..ee4c7d5 100644 --- a/docs/catalog/javascript-typescript.md +++ b/docs/catalog/javascript-typescript.md @@ -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) | @@ -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. + +### 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. + +### 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 diff --git a/docs/catalog/javascript-typescript.pt.md b/docs/catalog/javascript-typescript.pt.md index a223e38..7c9c7ed 100644 --- a/docs/catalog/javascript-typescript.pt.md +++ b/docs/catalog/javascript-typescript.pt.md @@ -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) | @@ -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 diff --git a/docs/catalog/python.md b/docs/catalog/python.md index 4633b1d..cf466a5 100644 --- a/docs/catalog/python.md +++ b/docs/catalog/python.md @@ -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 @@ -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 @@ -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. + +=== "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 @@ -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. + +=== "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 diff --git a/docs/catalog/python.pt.md b/docs/catalog/python.pt.md index 50e7602..f535759 100644 --- a/docs/catalog/python.pt.md +++ b/docs/catalog/python.pt.md @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/docs/catalog/robot.md b/docs/catalog/robot.md index 92ddbba..d8da864 100644 --- a/docs/catalog/robot.md +++ b/docs/catalog/robot.md @@ -43,6 +43,8 @@ nothing. | C7 | HIGH | self-compare (`Should Be Equal ${x} ${x}`) | | C44 | HIGH | library assertion provably true for any value (`Should Contain ${x} ${EMPTY}`, `Should Not Be Empty ${TRUE}`, `Should Be Empty ${EMPTY}`, a `Length Should Be` tautology) | | C9 | LOW | catch-all expected error (`Run Keyword And Expect Error *`) | +| C9b | LOW | a RequestsLibrary HTTP method with `expected_status=any` / `anything` — the request accepts every status, so the oracle is disabled and a 500 never fails | +| C11a | HIGH | self-confirming literal: an in-body copy of the actual feeds the expected side (`${y}= Set Variable ${x}`, then `Should Be Equal ${x} ${y}`) | | C16 | LOW | `Sleep` as synchronization instead of `Wait Until *`, `Get Current Date` (clock read), `Generate Random String` (randomness), or `Evaluate` with `datetime`/`random`/`uuid` | | C20 | HIGH | verification after a terminator (`[Return]`, `Return From Keyword`, `Fail`, `Pass Execution`) | | C21 | LOW | verification only inside `IF` / `Run Keyword If` that may not run | @@ -145,6 +147,34 @@ scanner cannot see). Page Should Contain Welcome ``` +### R8 - the only verification lives in [Setup] +`J1` · HIGH · F2 + +A test whose sole verification keyword sits in `[Setup]` (or a suite-level `Test Setup`). Setup +runs before the body acts, so it checks preconditions, not the result: the body can break and the +suite stays green. Move the assertion into the body. + +=== "BAD" + ```robotframework + Order Is Shipped + [Setup] Should Be Equal ${status} pending + Ship Order # body acts, never verified + ``` +=== "CLEAN" + ```robotframework + Order Is Shipped + Ship Order + Should Be Equal ${order.status} shipped + ``` + +### R8b - the only verification lives in [Teardown] +`J1` · LOW · F2 + +The sole verification keyword sits in `[Teardown]` (or `Test Teardown`). Teardown runs even when +the body fails and reports on a separate axis (it can flip a failed test's reason), so it does not +verify the body's result. Lower confidence than R8 because a teardown check is sometimes a +deliberate cleanup assertion. + ## Diagnostic codes (opt-in, OFF by default) `D2` (control flow in a test) and `M2` (over-long test) - hygiene, not false-green. Robocop also diff --git a/docs/catalog/robot.pt.md b/docs/catalog/robot.pt.md index 554f2ba..29618ef 100644 --- a/docs/catalog/robot.pt.md +++ b/docs/catalog/robot.pt.md @@ -43,6 +43,8 @@ nada. | C7 | ALTO | autocomparação (`Should Be Equal ${x} ${x}`) | | C44 | ALTO | asserção de biblioteca provavelmente verdadeira para qualquer valor (`Should Contain ${x} ${EMPTY}`, `Should Not Be Empty ${TRUE}`, `Should Be Empty ${EMPTY}`, uma tautologia de `Length Should Be`) | | C9 | BAIXO | erro esperado pega-tudo (`Run Keyword And Expect Error *`) | +| C9b | BAIXO | um método HTTP da RequestsLibrary com `expected_status=any` / `anything` — a requisição aceita todo status, então o oráculo fica desativado e um 500 nunca falha | +| C11a | ALTO | literal autoconfirmante: uma cópia do real no corpo alimenta o lado esperado (`${y}= Set Variable ${x}`, depois `Should Be Equal ${x} ${y}`) | | C16 | BAIXO | `Sleep` como sincronização em vez de `Wait Until *`, `Get Current Date` (leitura de relógio), `Generate Random String` (aleatoriedade), ou `Evaluate` com `datetime`/`random`/`uuid` | | C20 | ALTO | verificação depois de um terminador (`[Return]`, `Return From Keyword`, `Fail`, `Pass Execution`) | | C21 | BAIXO | verificação só dentro de `IF` / `Run Keyword If` que pode não rodar | @@ -145,6 +147,34 @@ scanner não consegue ver). Page Should Contain Welcome ``` +### R8 - a única verificação vive no [Setup] +`J1` · ALTO · F2 + +Um teste cuja única keyword de verificação fica no `[Setup]` (ou num `Test Setup` de suíte). O +setup roda antes de o corpo agir, então verifica pré-condições, não o resultado: o corpo pode +quebrar e a suíte fica verde. Mova a asserção para o corpo. + +=== "RUIM" + ```robotframework + Order Is Shipped + [Setup] Should Be Equal ${status} pending + Ship Order # corpo age, nunca verificado + ``` +=== "LIMPO" + ```robotframework + Order Is Shipped + Ship Order + Should Be Equal ${order.status} shipped + ``` + +### R8b - a única verificação vive no [Teardown] +`J1` · BAIXO · F2 + +A única keyword de verificação fica no `[Teardown]` (ou `Test Teardown`). O teardown roda mesmo +quando o corpo falha e reporta num eixo separado (pode trocar o motivo de um teste que falhou), +então não verifica o resultado do corpo. Confiança menor que a R8 porque uma verificação de +teardown às vezes é uma asserção de limpeza deliberada. + ## Códigos de diagnóstico (opcionais, OFF por padrão) `D2` (controle de fluxo em um teste) e `M2` (teste longo demais) - higiene, não false-green. O Robocop também diff --git a/docs/concepts/coverage.md b/docs/concepts/coverage.md index cd07daf..eaa5ce7 100644 --- a/docs/concepts/coverage.md +++ b/docs/concepts/coverage.md @@ -41,11 +41,11 @@ diagnostic group, off by default. | Family | Failure mode | Codes the ecosystem ships | Layer | |---|---|---|---| -| **F1** | Checks nothing (no oracle) | `C2`, `C2b`, `C2c`, `C27`, `JS2`, `JS6`, `R-empty`, semantic cases 10/11 | static + skill | -| **F2** | The check exists but never runs | `C1`, `C3`, `C19`, `C20`, `C21`, `C22`, `C43`, `CC`, `JS7`, `JS9`, `JS11` | static | -| **F3** | The check is trivial (always passes) | `C5`, `C6`, `C6c`, `C7`, `C8`, `C8b`, `C11a`, `C18`, `C34`, `C42`, `C44`, `JS15`, `JS21` | static | -| **F4** | Checks the wrong thing | semantic case 18, parts of `C6` / `C33` / the snapshot codes | static (partial) + skill | -| **F5** | Drops out of the count (skip / not collected) | `C4`, `C4b`, `C25`, `C32`, `C38`, `C45`, `JS1`, `JS4`, `JS22`; project layer: `PL1`-`PL8` | static + project layer | +| **F1** | Checks nothing (no oracle) | `C2`, `C2b`, `C2c`, `C27`, `C39`, `C50`, `C51`, `JS2`, `JS6`, `JS13`, `R2`, `R4`, `R7`, semantic cases 10/11 | static + skill | +| **F2** | The check exists but never runs | `C1`, `C3`, `C20`, `C21`, `C22`, `C43`, `CC`, `JS5`, `JS7`, `JS9`, `JS11`, `JS25`, `JS26`, `JS29`, `JS31`, `R8`, `R8b` | static | +| **F3** | The check is trivial (always passes) | `C5`, `C6`, `C6c`, `C7`, `C8`, `C8b`, `C11a`, `C18`, `C34`, `C42`, `C44`, `C52`, `JS15`, `JS21`, `JS30`, `R1`, `R6` | static | +| **F4** | Checks the wrong thing | `C9`, `C9b`, `C19`, `C28`, `C49`, `C55`, `JS8`, `JS24`, `JS27`; semantic case 18, parts of `C6` / `C33` / the snapshot codes | static (partial) + skill | +| **F5** | Drops out of the count (skip / not collected) | `C4`, `C4b`, `C25`, `C32`, `C38`, `C45`, `JS1`, `JS4`, `JS22`, `JS23`, `R3`, `R5`; project layer: `PL1`, `PL2`, `PL7`, `PL8`, `PL9`, `PL10` | static + project layer | | **F6** | Passes or fails by luck (non-determinism) | `C16`, `C23`, `C24`, `C29`, `C35` (static proxies) | static (proxy) + runtime | | **F7** | Circular or semantic oracle | semantic cases 10, 11, 12, 15; `C14` (the codable corner) | skill + mutation testing | | **F8** | Hygiene / readability (not false-green) | `D1`, `D3`, `D4`, `D5`, `D6`, `D7`, `D8`, `M2` (opt-in diagnostics) | diagnostic / linter | @@ -64,7 +64,7 @@ that names its primary mechanism. metric). The contradicts-the-spec core is semantic and lives in the skill (case 18); it is not a static count. - **F5** has two slices: the per-file slice (a test not collected, a non-strict xfail) counted in - the scanner codes, and the project slice (`PL1`-`PL8`, read by `--config-audit`) counted + the scanner codes, and the project slice (`PL1`, `PL2`, `PL7`, `PL8`, `PL9`, `PL10`, read by `--config-audit`) counted separately. The runtime slice (a collection error reported as "0 tests") is documented, not a code. - **F6** is counted only as static proxies (`C16` for uncontrolled time/randomness, `C23` for a diff --git a/docs/concepts/coverage.pt.md b/docs/concepts/coverage.pt.md index 17cceae..b7f8eee 100644 --- a/docs/concepts/coverage.pt.md +++ b/docs/concepts/coverage.pt.md @@ -41,11 +41,11 @@ diagnóstico, desligado por padrão. | Família | Modo de falha | Códigos que o ecossistema entrega | Camada | |---|---|---|---| -| **F1** | Não checa nada (sem oráculo) | `C2`, `C2b`, `C2c`, `C27`, `JS2`, `JS6`, `R-empty`, casos semânticos 10/11 | estático + skill | -| **F2** | A checagem existe mas nunca roda | `C1`, `C3`, `C19`, `C20`, `C21`, `C22`, `C43`, `CC`, `JS7`, `JS9`, `JS11` | estático | -| **F3** | A checagem é trivial (sempre passa) | `C5`, `C6`, `C6c`, `C7`, `C8`, `C8b`, `C11a`, `C18`, `C34`, `C42`, `C44`, `JS15`, `JS21` | estático | -| **F4** | Checa a coisa errada | caso semântico 18, partes de `C6` / `C33` / os códigos de snapshot | estático (parcial) + skill | -| **F5** | Sai da contagem (skip / não coletado) | `C4`, `C4b`, `C25`, `C32`, `C38`, `C45`, `JS1`, `JS4`, `JS22`; camada de projeto: `PL1`-`PL8` | estático + camada de projeto | +| **F1** | Não checa nada (sem oráculo) | `C2`, `C2b`, `C2c`, `C27`, `C39`, `C50`, `C51`, `JS2`, `JS6`, `JS13`, `R2`, `R4`, `R7`, casos semânticos 10/11 | estático + skill | +| **F2** | A checagem existe mas nunca roda | `C1`, `C3`, `C20`, `C21`, `C22`, `C43`, `CC`, `JS5`, `JS7`, `JS9`, `JS11`, `JS25`, `JS26`, `JS29`, `JS31`, `R8`, `R8b` | estático | +| **F3** | A checagem é trivial (sempre passa) | `C5`, `C6`, `C6c`, `C7`, `C8`, `C8b`, `C11a`, `C18`, `C34`, `C42`, `C44`, `C52`, `JS15`, `JS21`, `JS30`, `R1`, `R6` | estático | +| **F4** | Checa a coisa errada | `C9`, `C9b`, `C19`, `C28`, `C49`, `C55`, `JS8`, `JS24`, `JS27`; caso semântico 18, partes de `C6` / `C33` / os códigos de snapshot | estático (parcial) + skill | +| **F5** | Sai da contagem (skip / não coletado) | `C4`, `C4b`, `C25`, `C32`, `C38`, `C45`, `JS1`, `JS4`, `JS22`, `JS23`, `R3`, `R5`; camada de projeto: `PL1`, `PL2`, `PL7`, `PL8`, `PL9`, `PL10` | estático + camada de projeto | | **F6** | Passa ou falha por sorte (não-determinismo) | `C16`, `C23`, `C24`, `C29`, `C35` (proxies estáticos) | estático (proxy) + runtime | | **F7** | Oráculo circular ou semântico | casos semânticos 10, 11, 12, 15; `C14` (o canto codificável) | skill + mutation testing | | **F8** | Higiene / legibilidade (não é false-green) | `D1`, `D3`, `D4`, `D5`, `D6`, `D7`, `D8`, `M2` (diagnósticos opcionais) | diagnóstico / linter | @@ -64,7 +64,7 @@ listado sob a família que nomeia seu mecanismo principal. métrica descartada). O núcleo "contradiz a spec" é semântico e vive na skill (caso 18); não é uma contagem estática. - **F5** tem duas fatias: a fatia por arquivo (um teste não coletado, um xfail não-strict) contada - nos códigos dos scanners, e a fatia de projeto (`PL1`-`PL8`, lida pelo `--config-audit`) contada à + nos códigos dos scanners, e a fatia de projeto (`PL1`, `PL2`, `PL7`, `PL8`, `PL9`, `PL10`, lida pelo `--config-audit`) contada à parte. A fatia de runtime (um erro de coleta reportado como "0 testes") é documentada, não um código. - **F6** é contado só como proxies estáticos (`C16` para tempo/aleatoriedade descontrolados, `C23` diff --git a/docs/credits.md b/docs/credits.md index d1882cd..d1f7106 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -46,6 +46,34 @@ assertion never executes or never fails. - Fowler, M.; Beck, K. *Refactoring: Improving the Design of Existing Code.* 2nd ed., 2019. The vocabulary of smells and behavior-preserving refactoring. +## Detection tools (the landscape) + +How the field detects test smells, for context. Most of these target the maintainability axis; +the ones that touch the false-green slice (Conditional Test Logic, Empty/Default Test, Exception +Handling) are noted. + +- van Deursen, A.; Moonen, L.; van den Bergh, A.; Kok, G. *Refactoring Test Code.* CWI Report + SEN-R0119, 2001 (also XP 2001). The origin catalog of 11 test smells. +- Reichhart, S.; Girba, T.; Ducasse, S. *Rule-based Assessment of Test Quality (TestLint).* Journal + of Object Technology 6(9), 2007. Early static+dynamic rules, including unexecuted/skipped test + code - the rotten-green mechanism before it was named. +- Peruma, A. et al. *tsDetect: An Open Source Test Smells Detection Tool.* ESEC/FSE 2020. DOI + [10.1145/3368089.3417921](https://doi.org/10.1145/3368089.3417921). AST patterns, ~19 smells + including Conditional Test Logic, Empty Test, Exception Handling. +- Virginio, T. et al. *On the test smells detection: an empirical study on the JNose Test accuracy.* + JSERD 9:8, 2021. DOI [10.5753/jserd.2021.1893](https://doi.org/10.5753/jserd.2021.1893). +- Wang, T. et al. *PyNose: A Test Smell Detector for Python.* ASE 2021. DOI + [10.1109/ASE51524.2021.9678615](https://doi.org/10.1109/ASE51524.2021.9678615). +- Lambiase, S. et al. *Just-In-Time Test Smell Detection and Refactoring: The DARTS Project.* ICPC + 2020. DOI [10.1145/3387904.3389296](https://doi.org/10.1145/3387904.3389296). +- de Paula, E. A.; Bonifacio, R. *TestAXE: Automatically Refactoring Test Smells Using JUnit 5 + Features.* SBES 2023. Implements JUnit 5 refactorings including Conditional Test Logic. +- Pontillo, V. et al. *Machine Learning-Based Test Smell Detection.* arXiv:2208.07574, 2022 + (EmSE 2024). Argues heuristic detectors are threshold-fragile. + +The same-id convention across the falsegreen scanners (C5 is the always-true assertion in every +language) is the family's own answer to this fragmentation. + ## LLM-based detection - Peixoto, M. et al. *On the Effectiveness of LLMs for Manual Test Verifications.*