{
  "id": 143256,
  "title": "It had 100% coverage and yet production fell.",
  "url": "https://urgent.news/2026/08/04/tenia-100-de-cobertura-y-aun-asi-se-cayo-produccion",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-08-04T19:50:26.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/rrondan/tenia-100-de-cobertura-y-aun-asi-se-cayo-produccion-43oc"
  },
  "original_language": "es",
  "account": "Today, a large part of the code that goes into my projects is written by a model. It's not an experiment, it's how we work. And that brought me a question that I hardly ever asked before, because I wrote the code and more or less knew where the gaps were: if I didn't write this, how do I know it works? The obvious answer is \"I measure coverage\". I measure it. And once I had everything green while the page fell in production. This post is about that contradiction, and about the decisions that make the number mean something or mean nothing.\n\nFirst, what do I measure?\nNothing exotic: flutter test --coverage generates lcov.info and then I filter with lcov --remove before getting the percentage.\n\nlcov --remove coverage/lcov.info \\\n'**/*.freezed.dart' \\\n'**/*.g.dart' \\\n'**/*.config.dart' \\\n'**/constants/*.dart' \\\n'**/theme/*.dart' \\\n'**/di/*.dart' \\\n'**/router/*.dart' \\\n-o coverage/lcov_filtered.info\n\nWhat I take out of the account and why:\nThe generated files (freezed, json_serializable, injection configuration) I didn't write them. Testing them is testing the generator. If freezed is broken, that's not my test.\nConstants and theme don't have branches. There's no input that makes them behave differently. A test there only confirms that a constant is worth what it's worth.\nDI and routing are wiring. Verifying that the container resolves a dependency is testing the framework.\n\nThe rule, if I had to summarize it, is that I exclude what can't make a wrong decision. Everything else goes in.\n\nAnd here it's convenient to say something uncomfortable before someone else says it: the list of exclusions is where you can cheat. I can get to the percentage I want by moving those patterns. That's why I don't touch it when the number doesn't like me. If I have to justify why I took something out, the answer can't be \"because I didn't reach it\".\n\nThe test that runs everything and doesn't verify anything\nThis is the misunderstanding that makes experienced people distrust coverage, and they have reason to distrust it.\nlcov measures which lines were executed. It doesn't measure if you checked something. Those are different things and a model generating tests confuses them all the time.\n\ntest('applies discount', () {\nfinal result = calculator.applyDiscount(100, 0.2);\nexpect(result, isA<double>());\n});\n\nThis test runs each line of applyDiscount. Coverage: 100%.\nReal checks: zero.\n\nIf the formula were inverted, price * discount instead of price * (1 - discount), it would pass just as green.\n\ntest('applies 20% discount', () {\nexpect(calculator.applyDiscount(100, 0.2), 80.0);\n});\n\ntest('rejects discounts greater than 100%', () {\nexpect(() => calculator.applyDiscount(100, 1.5), throwsArgumentError);\n});\n\nSame line coverage. The difference isn't measured by any tool. It's measured by a question: does the test assert something, or just run?\n\nModels write the first one by default. That's what they do best: they go through the code, get to green, and raise the number.\n\nWhen I review a generated test, I don't look if the percentage went up, I look at what happens if I break the function on purpose. If the test still passes, the test doesn't exist.\n\nThe presentation layer doesn't go in, and it's not because of laziness\nThis was the decision that cost me the most and that I defend the most.\nI started writing widget tests like everyone else. Two things made me stop.\n\nThe first one is silly but real: the expects couldn't find the widget. Sometimes yes, sometimes no, depending on how the tree had turned out. I ended up fighting with the finders instead of verifying behavior. Anyone who's written widget tests in Flutter knows what I'm talking about.\n\nThe second one is what matters.\nIn the app, there are options that appear or not depending on the user's role, and there are quite a few cases. But that decision doesn't live in the widget: it lives in the BLoC. The widget only paints what the state tells it.\n\nSo if I test from the widget that \"user X sees option Y\", I'm verifying the same rule that I already verified in the BLoC, but through a slower door, more fragile and that breaks when someone changes a Padding.\n\nIt's not that I don't test the UI. It's that I don't test the same thing twice, and I choose to do it where the test is cheap and stable.\n\n(What I still lack, and I say it because an honest post also says where it didn't reach: golden tests for what is stable. It's on the list. I haven't done it yet.)\n\nLegacy: I don't pursue the past\nIn projects that I start from scratch, I aim to cover all the logic. But I also maintain inherited code, without tests, written by people who are no longer there. There, I don't pursue 100% backwards. It would take months to write tests for code that I'm not going to touch, and worse, tests that freeze the current implementation instead of protecting a behavior.\n\nWhat I do is simpler: the old code stays as it is, but everything new that I add to it comes in with its tests. The bar goes up every time I touch that module.\n\nI don't stop delivery to cover the past, I stop the bleeding.\nIt's the difference between \"this project has 100%\" and \"this project doesn't lose coverage again\".\n\nThe second one is achievable without stopping the world.\n\nThe fall\nAnd here's the part where the number lied to me in the face.\nMy suite runs on mocks. The entire data layer is mocked, so the tests don't talk to a server: they talk to a frozen version of what the server said the last time I wrote that mock.\n\nWe have a system of points for delivered orders. One day, due to a backend error, a delivered order didn't load its points and the field came null. It had never come null before.\n\nThe app couldn't handle it, the web couldn't either, and the page threw an error.\n\nAll my tests were green. All of them.\n\nAnd the reason is almost humiliatingly simple: my mock kept returning the old world.\nI had complete coverage of a reality that no longer existed.\n\nThe test never saw a null because I never wrote a mock that returned it, and I didn't write it because in my head that field always came.\n\nThat's the answer I give to anyone who says that coverage means nothing.\n\nYou have reason, and here's the proof from my own suite: 100% means...",
  "summary": "Hoy buena parte del código que entra a mis proyectos lo escribe un modelo. No es un experimento, es cómo trabajamos. Y eso me trajo una pregunta que antes casi no me hacía, porque el código lo escribía yo y más o menos sabía dónde estaban los huecos: si no escribí esto, ¿cómo sé que sirve? La respuesta obvia es \"mido cobertura\". Yo la mido. Y una vez tuve todo en verde mientras la página se caía…",
  "key_points": [],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}