The tragedy of 100% code coverage (2016)
labs.ig.com
labs.ig.com
Just because you ran a function or ran a line doesn't mean it will work for the range of inputs you are allowing. If your function that you are running coverage on calls into the OS or a dependency, you also have to be ready for whatever that might return.
Therefore you can't tell if your code is right just by having run it. Worse, you might be lulled into a false sense of security by saying it works because that line is "covered by testing".
The real answer is to be smart, pick the right kind of testing at the right level to get the most bang for your buck. Unit test your complex logic. Stress test your locking, threading, perf, and io. Integration test your services.
When I'm writing tests, I'm not so concerned about what the tool tells me about which lines are covered, I like to work through the mental exercise of knowing which basis paths are covered. If a function has too many for me to reason about, that is a problem in itself.
As an aside, while I'm rambling, all the examples in the article appeared to represent unnecessary abstraction, which is the opposite problem. If you have many methods in a class with only one basis path, what purpose does the class serve. These testing concerns may be the code smell that points to deeper problems
One other thing about code coverage, unit testing, and other testing fads is I think they actively affect the architecture, and usually in the overthinking it way.
Instead of having one tight bit of procedural code (which may have some state, or some dependency calls), people split it up into multiple classes, and then test each bit. This allows them to use the class architecture to mock things, but really has just multiplied out the amount of code. And in the end, you're running tests over the golden path probably even less. It's even possible to have 100% of the code covered, and not run the golden path, because you're always mocking out at least one bit.
I wonder what that would look like...
We should go a lot further though. IMO, a unit that does not pass a spec/test should cause a compile time error. Testing systems should facilitate and converge with formal verification. Where possible, property based testing should be used and encouraged. And debugger tools should be able to hone in on areas where the result diverges from expectations.
And another example of something I'd want checked by the language is exception throwing / handling. It's another one of those places where code coverage won't help you unless you already know what you're looking for. Languages are getting better about it, but in general, handling errors is hard.
[0] https://developer.salesforce.com/docs/atlas.en-us.apexcode.m...
We can achieve this in dependently-typed languages like Idris. First we define a datatype 'Equal x y', which will represent the proposition that expression 'x' is equal to expression 'y':
data Equal x y where
Refl : (x : _) -> Equal x x
There are two things to note:- There is only one way to construct a value of type `Equal x y`, which is to use the constructor we've called `Refl`.
- `Refl` only takes one argument, `x` (of arbitrary type `_`), and it only constructs values of type `Equal x x`. This is called "reflexivity", thus the name `Refl`.
Hence if we use the type `Expr x y` anywhere in our program, there is only one possible value we can provide that might typecheck (`Refl x`), and that will only typecheck if `Expr x x` unifies with `Expr x y`, which will only happen if `x` and `y` are the same thing; i.e. if they are equal. Thus the name `Equal`.
This `Equal` type comes in the standard library of all dependently typed languages, and is widely used. To use it for testing we just need to write a test, e.g. `myTest`, which returns some value indicating pass/fail, e.g. a `Boolean`. Then we can add the following to our program:
myTestPasses : Equal myTest True
myTestPasses = Refl myTest
This will only type-check if `Equal myTest myTest` (the type of `Refl myTest`) unifies with `Equal myTest True`, which will only be the case if `myTest` evaluates to `True`.In haskell you embed domain specific languages when you require side effects:
class MonadState s m where
get :: m s
put :: s -> m ()
This is like an interface specifying the basics of all stateful computations.You can use different implementations for production and testing without changing any code so mocking is built into everything.
http://dlang.org/spec/unittest.html
It's been very effective at improving the overall quality of the code. And because of CTFE (Compile Time Function Execution) and static assert, many code correctness tests can even be run at compile time.
On .NET it is a plain library, which requires the VS Ultimate editions to be useful and on Java it was mostly third party libraries.
C# design team is considering adding proper support as language feature, but it seems to be a very far away feature still.
C++20 might get contracts, but C++17 just got ratified, so who knows.
D, Ada 2012 and SPARK do already support contracts.
Thus it is often a test of the implementation's assumptions when first written, and even worse, when the code is maintained/edited, the test is merely changed to get it to pass, because unit tests with mocks are usually:
1) fragile to implementation 2) opaque as to intent
Whereas input/output integration points are more reliable, transparent, and less fragile to implementation changes if the interface is maintained.
However, if you must do mock-level interaction testing, Spock has made it almost palatable in Javaland.
This is one area where functional fans get to make the imperative folks eat their lunch.
printf("hello world");
And the test is: mock_printf(string) {
if string != "hello world" then fail;
}
Which is basically just duplicating your code as tests.Usually because interfaces need to be clearly/well defined and overridden... In JS there are tools (rewire, proxyquire, etc) that can be used with tests in order to easily inject/replace dependencies without having to write code to support DI nearly as much. In fact, I'd say that there's usually no reason not to be very close to 100% coverage in JS projects.
It ties together related algorithms. You want "addDays(n)", "addHours(n)", "addMinutes(n)" and so on to be in the same class, even if they're one-liners.
I note that the examples you give have trivial test cases - presuming you don't care about overflow, and if you do, then those methods now have basis paths, whether explicit or implicit depends on the language.
"Pex uses a constraint solver to produce new test inputs which exercise different program behavior. "
So this is not lines, but I guess.. branches and numerical limits. Either way it's cool and I wish it was integrated somewhere. They've got a online demo thing http://www.pexforfun.com/
[1]https://www.microsoft.com/en-us/research/publication/pex-whi...
[0] https://blogs.msdn.microsoft.com/visualstudio/2015/09/30/int...
To prove it works with all possible inputs, there are other tools at your disposal.
I'd like to hear about real world test scenarios where all possible inputs are tested.
The input fuzzing process is rarely purely random. There are advanced techniques that allow the fuzzer to link input data to conditions of not covered branches.
It is quite useful mechanism for checking inputs, formats, behaviour patterns (if you have two solutions but one model, one simple that works 100% but is slowish and one more complex but very fast).
See: https://github.com/dvyukov/go-fuzz#trophies and http://lcamtuf.coredump.cx/afl/
What I meant was what @AstralStorm said about mathematical and logic proofs that it works for all defined values.
Proofs are powerful, but you can still make errors in the proof or the transcription into code. And of course you don't know what other emergent behaviours will surprise you when the proved code interacts with unproved code.
Depending on your means and needs, you want to try both.
A sane type system would be a start for example. And it's no coincidence that functional programming and practices with emphasis on immutability are on the rise; Rusts ownership system is a direct consequence as well.
TDD _is_ important, if simply for enabling well-factored code and somewhat guarding against regression bugs. But - decades after Dijkstras statement (which someone has already posted in this thread) - the code coverage honeymoon finally seems to be over.
Brute force.
Oh other story. An OG (original geek) I know once proved the floating point unit on a mainframe was broken via brute force. Bad part meant the decimal floats used by and only by the accounting department sometimes produced erroneous results.
Me I have a pitiful amount of unit tests for the main codebase I work on. One module which does have unit tests, also had a bug that resulted in us having to replace a couple of thousand units in the field.
Otherwise most of the bugs that chap my keister aren't about procedural correctness, they're about state.
A + B == B + A
A + 0 == A
(A + B) + C == A + (B + C)
A + B != A + C (given B != C)When you have one available, testing against the results against a reference implementation is a little more direct. Though, given how easy those property tests are, you might as well do both.
FLOAT_EQ(sqrt(x) * sqrt(x), x) && sqrt(x)>=0.0
is a pretty good test for the square root even though it doesn't compute it.Some times I have, for example, matlab code that I 'know' is right and want to verify that my C/Python/Julia... code does the same thing. So then I just call the matlab code, call the new code and check if they're the same.
Or I have a slow exact method and want to see if my faster approximation always gives an answer 'close enough' to the correct answer.
This sounds pretty sketchy at first, but it turns out that almost all of the difficulty (and hence almost all of the bugs) are in getting the last few bits right, and the last few bits of a much-higher precision result don't matter when you're testing a lower-precision implementation, so combined with some basic sanity checks this is actually a very reasonable testing strategy (and widely used in industry).
However, I don't run that as part of the normal test suite. It only gets run when I'm prepping a new release, as a final verification step; the normal runs-every-commit test suite just exercises with a selection of values likely to expose obvious problems.
https://github.com/ubernostrum/webcolors/blob/master/tests/f...
The fun part is people who criticize the generation of the integer triplets; yes, it's three nested loops and that's bad, but the total number of iterations will always be 16,777,216 no matter what algorithm you decide to use to generate them. So it uses nested loops since that's the most readable way to do it.
The hard part is the percentage rgb() values, of which there are technically an uncountably infinite number (since any real number in the range 0-100 is a legal percentage value). For those I generate all 16,777,216 integer values, and verify that converting to percentage and back yields the original value.
You're also making it sound like somebody promised you that tests can prove the absence of bugs, when that was never the bargain and smart people have already told you so, probably before you were born even.
> Testing shows the presence, not the absence of bugs
Dijkstra (1969)
Definitely not a tragedy.
I think when you take all of the time wasted on useless tests written merely for the sake of having tests, that waste is tragic. You could be doing anything else with that time.
It seems to me that waste is unavoidable with teams newer to automated testing and may simply be part of the cost of using automated tests. If that is the case then it seems better to compare the cost of bugs with no automated to cost of superfluous tests. In that comparison extra tests definitely seems like the lesser of two evils, even without hard numbers. I would prefer hard numbers because my intuition could be wrong.
1. Take the number of hours spent writing tests.
2. Multiply by whatever percentage of the tests are unnecessary.
3. Multiply by the labor cost per hour. Or revenue that was not made (e.g. if you could have billed those hours to a customer, but didn't).
The resulting number could either be a big deal or not, depending on how big your organization is and how much time you spent on superfluous tests.
Automated tests can do so much more than manual tests that shops still living by manual tests either have a damn good reason or are just as wrong as people who argued against Revision Control Systems (or people who argued for gotos instead of functions).
Automated tests will be executed identically each time, so no missing test cases because someone slacked off or made a typo. Automated test can serve as examples for how to use the code. Automated tests can aid in porting to new platforms, once it builds you can find all the bugs your care about swiftly. Automated test can be integrated with documentation, tools like doxyegn and mdbook make this easier.
Automated tests enable Continuous Integration. Are you familiar with Travis CI or Jenkins? If not, imagine a computer that a team commits their code to, instead of directly to the mainline Revision Control (Git master, svn head, etc...). That computer builds the software, runs all the tests, perhaps on every supported platform or in an environment very close to production, then only merges commits that appear to fully work. This doesn't completely eliminate bugs and broken builds, but the change is so large that teams without it are at a clear competitive disadvantage.
When integrated into process tests can be used to protect code from changes. If a test exercises an API and the team knows that is the purpose then when they change things in a way that break the test they shouldn't... This sounds vague or obvious, but consider this: At Facebook and Google they have a rule that if it is not tested new code doesn't have to care if it breaks. Both companies have team that make broad Sweeping changes. Facebook wrote a new std::string and Google use clangtools to make automated changes in thousands of places at once. Even if code breaks or APIs change as long as tests pass these people can be sure that they negatively impacted the product and are following their team's rules.
Automated Tests can... This list could go on for a very long time.
I think it should be a mix. I disagree with the "extreme minority" portion for most projects. There are times where a manual test is the right answer and there's exploratory manual testing.
Obviously though running through a long sequence of testing manually for every code change is crazy. And some sort of CI setup like you describe is a must for every project this day and age.
Then there's also the question of unit tests vs. system/integration tests...
Why not prove the code is correct instead? Should be much cheaper than 100% coverage and more certain.
Mutation testing systems normal use fairly stable operators (e.g changing a > to a >=). In most locations in the code changes such as these will have only a subtle effect.
It seems to me you need to have serious OCD to go for 100% mutation coverage, but that is what you really need to do if perfect coverage is your aim.
The other option is to accept that perfect coverage is not feasible, or possibly even a trap, and that you just need to cover the interesting bits.
The halting problem is an irrelevant, Ivory-tower distraction in production code.
That's the idea at least. All focus on high coverage, whether line coverage or mutation testing, creates an incentive to remove redundant robustness checks, and maybe that's not such a good idea after all. But that's a problem with all unit testing, and not just with mutation testing.
If this change doesn't cause a test to fail, you're not testing edge cases for this comparison.
(Of course that's assuming you want perfect coverage.)
while ( j < JMAX) {
i = callback(j);
if( i >= FINAL) break;
...
If, in the specific circumstances of this use, callback(j) will always equal FINAL before it exceeds it, a test for equality here will not cause the program to behave differently.The fallacy of your argument is that == is not an assertion that its arguments should always be equal; it is a test of whether they are at some specific point in the algorithm.
On the other hand, if you do cover all your inputs, you've covered all the cases regardless of what % code coverage or path coverage you have.
1) 100% path coverage is not even close to exhaustively checking the full set of states and state transitions of any usefully large program.
2) If, furthermore, you have concurrency, the possible interleavings of thread execution blow up the already-huge number of cases from 1) to the point where the latter look tiny in comparison.
3) From 1) and 2), it is completely infeasible to exhaustively test a system of any significant size.
The corollary of 3) is that you cannot avoid being selective about what you test for, so the question becomes, do you want that decision to be an informed one, or will you allow it to be decided by default, as a consequence of your choice to aim for a specific percentage of path coverage?
For example, there are likely to many things that could be unit-tested for, but which could be ruled out as possibilities by tests at a higher level of abstraction. In that case, time spent on the unit tests could probably be better spent elsewhere, especially if (as with some examples from the article) a bug is not likely.
100% path coverage is one of those measures that are superficially attractive for their apparent objectivity and relative ease of measuring, but which don't actually tell you as much as they seem to. Additionally, in this case, the 100% part could be mistaken for a meaningful guarantee of something worthwhile.
[1] https://en.wikipedia.org/wiki/Equivalence_partitioning
I came across the concept some years ago, IIRC, in the classic book The Art of Software Testing by Glenford Myers [2], and used it in a few projects. But have not really heard or read of it being used much, from people or on forums.
[2] https://en.wikipedia.org/wiki/Glenford_Myers
I think it can be complementary to some of the other approaches mentioned in this thread, which, BTW, is interesting.
Alternatively, identifying equivalence partitions from the semantics of the input data faces the problem that a faulty program may create invalid partitions, so some test cases for a given equivalence partition pass, while others fail.
I had used the approach in your second paragraph above.
I agree that it can have the issue you mention. But is this not more or less the same as the issue that even test code can have bugs in it? But we still use test code. For that matter, even human testers doing manual testing can make mistakes. But we still do manual testing.
Agreed :) Good discussion.
Instead of writing clean code that makes sense and is easy to reason about, he will write long-winded, poorly abstracted, weird code that is prone to breaking without an extensive "test suite" to hold the madness together and god forbid raise an alert when some unexpected file over here breaks a function over there.
Tests will be poorly written, pointless, and give an overall false sense of security to the next sap who breaths a sigh of relief when "nothing is broken". Of course, that house of cards will come down the first time something is in fact broken.
I've worked in plenty of those environments, where there was a test suite, but it couldn't be trusted. In fact, more often than not that is the case. The developers are a constant slave to it, patching it up; keeping it all lubed up. It's like the salt and pepper on a shit cake.
Testing what you do and developing ways to ensure its reliable, fault-tolerant and maintainable should be part of your ethos as a software developer.
But being pedantic about unit tests, chasing after pointless numbers and being obsessed with a certain kind of code is the hallmark of a fool.
Made one small change and everything broke.
Looked into the tests, oh the humanity!!!
Dropped the test suite entirely, haven't had any issues since.
We have VERY good logging and notifications though so anything goes wrong we know all about it.
That you know about.
That bad test suites exist is unarguable.
Bad production code exists too, but we don't give up on writing production code.
It would definitely help in maintaining the code (not to mention not having to go through the entire code base when debugging a production issue).
As always, generalization is the tool of the fool (sorry for the fool part, but it rhymes ;) ). Writing pointless stubs / mocks and testing execution order of statements is definitely a bad pattern, writing many and good functional, e2e and integration tests however is not.
Also worth a read while we're at it: https://www.linkedin.com/pulse/20140623171038-114018241-test...
When You are cleaning up tech debt and make hundreds of lines of changes and no acceptance tests break (because everything is still working as it should) but 50 unit tests break... I've wasted so much of my life
To a first approximation, nobody is being empirical, and there is no science being done.
When programmers change a factor to "see what breaks", that is very much an empirical activity, and it is part of the programmer's theory-building of a phenomena.
If a young child takes a gear from a watch and observes its breaking, that is very much an empirical activity. It is also the beginning of theory-building.
You don't need MANOVA to engage in empiricism.
Approximately nobody is being empirical about what works well and does not in testing. Use this approach vs. that approach, do this, no - do this instead. It's all largely heuristic.
Individuals performing testing and debugging are usually at least most empirical, I agree (although occasionally the rubber chickens come out)
Spare us your platitudes. Most developers don't decide the code coverage, the manager or the client does.
In the case of contractor work where a customer actually buys the code and not just the functinoality - it's very tricky. I have never done contractor work so I'm not aware of how contracts are usually written. How do you set a quality metric? I'd be much more comfortable to agree to third party to judge the quality than to have an arbitrary metric in a contract for e.g. ratio number of comments to code lines, the average number of character in symbol names or the percentage of lines of code covered by tests.
That is my understanding, as it is rather hard to extract a solid argument from his posts.
The worst code bases I've seen have more test code than code being tested. People try to write tests to cover every condition or input and in systems of even modest complexity that isn't possible, or at least not feasible in a finite time.
Tests do have a maintenance cost, but it is much lower than the maintenance cost of code without tests. Without manual tests if there a bug it must be caught manually, this costs human time every time the tests are run. That also presumes the humans do the tests correctly, have a new team, QA person out sick, what is the cost now?
The cost of not having tests is bugs in production. If I write code and there a bug that impacts production many millions of dollars are on the line, and this is true for many developers. Write software for any airplane? Crashes can causes crashes so they are dangerous and expensive. How about something more common, Write software software for an online store? If the shopping cart drops 1 in 1000 items that is a huge amount of money, not just because of lost sales, but also angry customers. You will want to run the tests at least a few thousands times to catch that, a human won't do this, an automated unit test suite is shell script and VM away from doing that.
Automated tests don't make this impossible but it reduces the amount of bugs that can make it through. If a test costs 10 hours of developer time at $500/hour and stop one bug that would have alienated 1% of customers. Then there would only need to be 500,000 customers worth $1 each. Clearly these numbers have insanely inflated costs, yet they still make sense for many businesses. And this is only accounting for one bug. Finding that bug did not consume the test, it is still ready for more. A good test can many bugs and last many years.
Also, the notion that "they test each other" is likely to be dangerous.
About as dangerous as double entry bookkeeping. Of course it doesn't provide any absolute guarantees, but having people state things in multiple contexts and checking their consistency is one of the better approaches we have for finding errors.
This is not only wrong, but also dangerous.
I agree with the second part of your comment (about being pedantic about the number of unit tests), but a good and comprehensive test suite is fundamental for large projects, especially projects with many moving parts and a large turn-over of people (basically any big enterprise).
Unit tests only really catch a tiny fraction of bugs relating to defective code, not poor design, poor market fit, poor understanding of requirements, or any of the other bugs that most often make it into production. They can be a huge benefit to many possibly most projects, but there are no silver bullets, good quality needs competent people given sufficient time and resources and nothing really changes that equation.
Hint: anecdotal fallacy
Ed: Unit Tests become more valuable as code quality decreases.
He was not afraid to look you in the eye and say: "Your crappy code is not going into my codebase until you fix X, Y, Z, and change your indents to match the style guide." There were no "unit tests", per se, only a test driver program that he had written himself.
He was a hard guy to like, but I had to admit, that system had a clean, coherent, and modular design the likes of which I haven't seen since.
That's because unit tests aren't supposed to be a magical silver bullet that automatically fixes everything. Unit tests are like an exercise regimen. Just because you work your ass off in the gym doesn't mean that you'll get the result you want. If you're still eating a massive amount of calories, have inconsistent workout patterns, or do exercises incorrectly, you will only see minimal gains. Likewise, if your unit tests are not testing the correct logic, it's just an exercise in futility.
People make jokes about the infamous null test:
Person p = new Person();
p.setName("a");
p.setAge(0);
assertNotNull(p);
assertNotNull(p.getName());
assertNotNull(p.getAge());
Yet there are a non-zero number of production code bases with those kinds of tests attached to them. And I firmly believe that this is one of the drivers of hatred towards unit tests: people see poor unit tests like the above, so they instinctively write off unit tests as a waste of time.Just because someone uses a tool incorrectly doesn't mean that the tool does not have value. A spudger isn't going to drive a nail into the wall, but that's not what a spudger was designed for.
What about all the shitty 30 year code bases with no tests and no documentation that got dropped because they were garbage.
It's not impossible to write good code without tests, it is just more likely that code with tests and docs will be good enough to survive 30 years. Your project with no tests or docs and was good might be 1 in a million, while software that is good with tests and docs might be 1 in 100.
If a competent programmer cannot quickly determine how many factors are in play, or if that programmer cannot constrain the number of relevant factors to a humanly manageable number, then I question how insightful the sum of tests were.
They were a 100% test shop. The site itself was broken, certain parts of it would literally crash the web server for certain users because the code was so terrible...but they had tests for everything.
What's worse, when fixing some of the broken code they got mad at me for making it harder to write tests...because somehow in this mindset a working product was less important than working tests. I've been wary of test nazis ever since.
no.... please..... no..... let's not go down this path. Why does everything have to be extreme? No tests, 100% tests... it's all bollocks.
The most robust code I've ever written was 100% because of unit tests. It was a little engine that approved the prior authorization for medications. The unit tests didn't cover 100% of the application. In fact the only bit it did cover was the various use cases in the approval logic. The tests were invaluable in writing the logic, and continued to be invaluable for the maintenance of the project.
Tests are a tool. You don't need to use the tool for EVERYTHING, but sometimes it's the best tool for the job. Using it doesn't make you a bad developer, using it badly makes you a bad developer.
* Bad coding practices lead to bad tests * Bad test practices make development very difficult, since you're constantly chasing your tail
They're invariably intertwined. Bad tests can be re-written. Bad code can be rewritten/refactored too, but it tends to have a larger impact on the product (you know, the thing that actually makes you money).
However, it seems that a lot of posters see bad tests and say that it's the problem, and therefore most tests are bad. They neglect that bad tests are a smell that what they're testing is probably shit. They're treating (condemning) the symptom, not the cause.
This is obviously wrong.
> Tests will be poorly written, pointless, and give an overall false sense of security to the next sap who breaths a sigh of relief when "nothing is broken". Of course, that house of cards will come down the first time something is in fact broken.
That's a generalization that does not apply everywhere.
>This is obviously wrong.
What makes it obviously wrong? It seems to be a personal opinion.
>That's a generalization that does not apply everywhere.
Does a comment necessarily have to contain statements that apply to every single person in every single situation at every point in time? I think we'd have to delete pretty much all comments on this website then.
It sounds like you have never worked on a project with no tests and those same developers that write "write long-winded, poorly abstracted, weird code that is prone to breaking". A test suite doesn't eliminate bad code or bad coders, but it does give the better people on the team a handle to grab them by.
Some of the worst developers I've worked with never wrote any tests. Like zero tests. Actually in one case negative number of tests; the engineer deleted tests I wrote because they were failing the build because of this engineer's (breaking) changes.
Every good developer I know writes tests.
The real Real Tragedy of 100% coverage is the number of shops who think they're done testing when they hit 100%. I've heard words to that effect out of the mouth of a test manager at Microsoft, as one example. No, code coverage is a metric, not the metric. Code coverage doesn't catch the bugs caused by the code you didn't write but should have, for example. Merely executing code is a simplistic test at best.
Tests have diminishing returns. You want to hit the absolute most crucial ones that give you plenty of bang for buck and even save you time. That means finding the (usually small handful) of functions that implement your most crucial and most complicated business logic, and writing tests for them.
Anything past that is for companies with customers that need to maintain a certain level of quality and service. Worry about it when you get there.
This sort of basic level of decision-making for testing is something I wish I had, but all the tutorials and guides are about 100% code-coverage TDD so it's hard to find a path to to learn reasonable, high ROI testing.
For me, a few references I use are "tests are a thinking help for specifying behavior", "tests hold behavior in place" and "test until you feel comfortable".
The first is a note that TDD thoughts are written by developers that write APIs, not by developers that write applications. TDD is a fantastic tool for an API designer, because they force you to think about the experience of using the API. So, whenever I design APIs, I like TDD. This is also a good argument for why you should minimize setup code that "goes behind the scenes" - if you're testing, say, a REST API, do as much of the setup and assertions as you can via the REST API as well!
The second helps me remember to think "a year from now, are all the behaviors this code needs to have obvious, or is someone likely to unintentionally break it?". I try to write tests that will flag if someone broke an important edge case - or the main use case! Tests can be used as the programmers equivalent of a carpenters' clamps, kind of.
The third is why I don't write as many tests anymore. I normally try to write one workflow-oriented feature test up front, like "When a user creates a new invoice, then that invoice should show up in the users list of invoices with the values they entered, plus x,y,z auto-generated parameters". As I implement the feature, if I come across a piece of logic that makes me feel uncomfortable - lots of branching, or code that's very important that it stays intact - I'll write a unit test or two to hold that in place; sometimes I don't write any unit tests, meaning I'd have written just one or two tests over the course of two or three days of implementation.
The place where you make money is the place that will have the largest demand for new and changing functionality. And where things change the most is where you need tests to protect against regressions.
Generally, most architectures in this domain have a combination of UI components, a data store, a set of update logic for the data store, and a set of asynchronous controllers that respond to events, interact with APIs, and call the aforementioned update logic.
In React the UI components are declarative, they (generally) contain no logic or algorithms, just a mapping from state to DOM. I see basically zero value in testing these. Bugs are almost always of the 'forgot to actually implement' variety, or are related to the way the page is rendered in a particular browser, rather than the DOM output the components are responsible for.
The data store update logic is usually either simple setters/getters (which don't need testing) or complex data transformations (which do).
The controllers also come in simple and complex varieties. Simple ones (one API call, one data store update once it's resolved) don't need testing. Anything more complex than that probably does.
So those are the two main targets for testing in the apps I build. I generally don't bother with anything else.
There are exceptions though. For example, here's an accordion UI component I built which relies on an asynchronous manual DOM update after the React DOM update has finished resolving. This could almost definitely use tests, if only to help any maintenance developers understand what it's doing.
https://gist.github.com/JonathonAshworth/b401810b965149348d0...
Basically, as long as you have some sort of sane architecture, there should only be a few potential targets for testing, and they should be easily identifiable.
A simple example of this is the bowling game kata: given the throws in a bowling game, calculate the (final) score. The 'hard' part is to keep strikes and spares in mind, including the bonus throws when a strike or spare is scored in the 10th frame.
If you were making an application that would help you keep track of your bowling score, that score calculator would have the highest ROI in terms of testing.
I also have a simple integration test that just clicks through everything and makes sure nothing crashes.
Not having a lot of tests can be painful. Especially when you're learning new languages or frameworks, you almost always want to go back and rewrite some code (or just reorganize it into different files), and it's really nice to have tests when you do that. So sometimes that gives me the motivation to start writing up a bunch of tests, and then after that I dive in and refactor everything.
I mean I wouldn't settle for less than 200% test coverage on an automatic pilot for landing airplanes. If it is a one off script that happen to become a part of a temporary business process perhaps just a sample set of data, a desired output and small tool for comparing the results is enough.
Throughout my career I find tests that tests the very lowest implementation detail, like private helper methods, and even though a project can achieve 100% coverage it still is no help avoiding bugs or regression.
Given a micro service architecture I now advocate treating each service as a black box and focus on writing tests for the boundaries of that box.
That way tests actually assist with refactoring rather than be something that just exactly follows the code and breaks whenever a minor internal detail changes.
However occasionally I do find it helpful map out all input/output for an internal function to cover all edge cases. But that's an exception.
Unit testing: Test all methods and paths of a class, even private ones.
Functional Testing: Test the public api of a class/service only. If something is wrong internally, it will be caught without having to write countless of little tests.
ROI of functional testing is high, as it is usually done with real data. In my opinion unit testing is a huge waste of time. Most of the tests devolve int mock objects, calling mock methods, and doing this that really don't help to find real world bugs, where two unit tests pass, but their methods produce the wrong output.
- If you want to know if your utility classes and functions are sane, unit testing is far better bang for your buck than trying to figure out whether they're being adequately exercised in your service tests.
- If you're trying to figure out which part of a complicated system broke, having unit tests that break on the specific module, or class, or method can be quite helpful.
- Yes, integration tests can be mocked up to look like real world data, and of course you can even feed them real data. The flip side is that their data requirements can be heavy, and they can be quite cumbersome to set up.
I think any test strategy – unit, integration, e2e, acceptance, UI – pursued to exclusion is a bad idea. Different projects and different teams call for different balances between them.
I think we talk about the same thing, sometimes I will test something internal.
I've thought about what I'm doing as considering larger "units" in my unit testing, but perhaps "functional testing" as parent introduced is what I'm advocating. I see this as distinct from integration testing. In a micro service architecture, my integration test would be chaining multiple services together into test scenarios.
To me, unit tests are when I'm testing the behavior of a "thing" in "isolation", for some definitions of those terms – and yes, I agree that those definitions vary greatly. However, I don't agree with you that the flexibility of the definition is an issue.
Integration testing for me is when I'm testing system interfaces, so I'm focusing on how my systems behave when they come together. Sometimes I do this in isolation, using stub services or even internal stubs to simulate another system's behavior, but generally I do it with an actual system when it's convenient.
I don't generally use the term functional testing because I personally think it's ambiguous – I'm testing functionality in either case! But I suspect our differences really just boil down to how you slice it. If you prefer to divide tests into black-box vs white-box, but don't care as much about the specific level of isolation involved in the test, functional testing is perhaps the term you'd prefer. I prefer to categorize tests in terms of the amount of isolation I'm using, in which case I'm basically thinking unit, integration, e2e.
This is actually where doctests work pretty well as they both document and test.
"If you're trying to figure out which part of a complicated system broke, having unit tests that break on the specific module, or class, or method can be quite helpful."
I don't find tracking down bugs with a repeatable test case to be much of a problem (events in live systems are a different story). It becomes even less of a problem if you sprinkle some assertions around the code that block off invalid code paths.
"Yes, integration tests can be mocked up to look like real world data, and of course you can even feed them real data. The flip side is that their data requirements can be heavy, and they can be quite cumbersome to set up."
Building mocks in unit tests is usually even more cumbersome and tedious.
For overly large sets of real world data I've had some success taking live database snapshots and cutting out 95% of the data before using the cut down to size dump for testing.
Which is just as ambiguous. Personally I stick with the single assert principal (which may be more than one literal assert) so that whatever I'm asserting is the behavior that this test is verifying.
You don't test private API, that's why they are declared private, it makes absolutely no sense to test private members.
Now the use of the private member might lead to a different code path : it has to be tested.
Unit Testing : test one unit ( a class for instance ) in isolation, which means all collaborators (the classes the tested class depends on) have to be stubbed or mocked.
Functional Testing : test multiple unit at the same time to check the behavior of an entire functionality, that's why it is called functional testing. Stub the IO.
End-to-end testing: Test the entire system with the IO.
Don't agree. I think this is something that people tell themselves to justify the anti-pattern that is private members being nearly impossible to test directly in a variety of languages, but it seems like nonsense. I've never seen a real case for excluding private methods as a testable unit.
Here's the reason:
We're not writing tests; we're writing SPECIFICATIONS.
Private methods are implementation details; they're not part of the specification.
Thanks for that thought though, definitely something I'll think on :)
That's how the unit you're testing is used - through it's public API. The public API is the specification of how it works.
By only testing the public API, you allow yourself maximum ability to refactor in the future, while still maximizing code coverage. It means that simple refactoring (inlining methods, for example) won't break tests. More importantly, a failing test means something is wrong that is potentially relied upon elsewhere. If you test private methods, you will get test failures without the public API of the unit having changed at all.
Your point about "each function is a unit" is fine, you can justify testing private methods with that - but it's inefficient. If it's not necessary to be in the spec (public API), why have you made it so? You're over-complicating the design by locking yourself into implementation details in places where you don't need to.
In my experience I've found that testing private methods directly is a code smell. It shouldn't be necessary.
That's not potentially one philosophy, that's the definition of unit testing.
> Yet that still doesn't preclude private methods from being an independently testable unit
Then these methods ain't private as they break encapsulation. You can't have it both ways. Either a method is private or it isn't.
> Implementation details are the meat of the whole thing
But unit testing isn't about testing implementation details, it's about testing a specification that your API must respect, because that's the behavior collaborators consuming that API rely on. If your collaborators can call a private API then that API isn't private at first place.
The distinction I always try to make is whether you're testing a contract or an implementation. If you're testing a contract, ROI is likely to remain high even as you move to finder granularities. If you're testing an implementation, so that the tests fail even when the implementation is 100% correct, then ROI falls off a cliff.
That's exactly what happens when the "unit" gets too small. If certain private methods can't be called a certain way because of constraints imposed by the class's public methods, then testing those calls is testing things that can't happen. That takes time away from testing (and implementing fixes for) things that can happen. If the class's contract changes so that those internal methods can be called in new ways, then yes, a 100% code-coverage unit test might catch the error. So, in all likelihood, would the new functional test accompanying the public-method change. The delta between the two, times the likelihood of such a scenario occurring in the first place, is too small to justify the cost of writing those tests and the likelihood that they'll generate false negatives.
I disagree with this. Unit testing means testing the "unit" (i.e. the class/object) but still at a boundary level: the public API of that class. You should mock out any dependencies that object would have to ensure you are only testing that class but the class API has to be respected.
A private method is a hidden implementation detail, testing those would be overfitting your tests to the current implementation meaning if you change one character anywhere in your source you will almost certainly have to change one or more tests. Plus, if your tests are so tightly coupled to the implementation, it's likely to suffer from any bugs the implementation does causing them to be hidden (never forget that tests are also code and therefor have bugs at a similar rate to any other code).
Writing test code requires the same level (if not more) of engineering discipline that writing the code does.
>ROI of functional testing is high, as it is usually done with real data.
I also disagree with all of this. Functional testing is a kind of sanity check that the parts actually work together once assembled. If you have proper unit test coverage (and properly designed/engineered tests!) the functional testing is basically checking configuration. The problems with function testing are (1) testing is about checking code paths but which paths functional tests take can be hard to predict and differ between subsequent runs, making it hard to make any statement about what passing actually means. (2) is exactly what you mentioned as a positive: people tend to want to use what they call "real" data, i.e. data they have actually seen before. Which means it's probably only good for catching bugs they know about, not ones they've never seen before.
>In my opinion unit testing is a huge waste of time.
I would actually agree to this because I think answering the question of "how do we detect bad code" with "write more code" is problematic. I'd rather go the Haskell/Idris route and be able to prove that I have no bugs.
>Most of the tests devolve int mock objects, calling mock methods, and doing this that really don't help to find real world bugs, where two unit tests pass, but their methods produce the wrong output.
This kind of response sounds like a self fulling prophecy. The exact point of mocking is to test code paths, i.e. if this dependency returns this result how will the class under inspection behave in response. Ideally you would use mocks to test everything that every dependency could respond. Unless the types involved have very few inhabitants, this isn't generally possible (even programatically) but the closer you get to this the more you can trust your tests.
An example of that exception: just yesterday I wrote tests for a serialization/deserialization utility, that translates between an object tree and a wire format used by our client. There was some tricky code around the "deserialize message into one of many possible objects" part, so I wrote a bunch of unit tests for the whole thing.
I find myself writing unit tests mostly for this kind of code - complex logic that transform data and/or execute "advanced" algorithms on it. I also write tests as close as possible to the boundaries of the complex logic - this way, when I'm sure the logic itself is sound, all integration bugs tend to be trivial to notice (it means someone fucked up the inputs or didn't handle outputs properly).
The more I think of it, the more I notice that I tend to structure my programs in a functional (as in functional programming) way - lots of "services" that take things as inputs and return them as outputs, without using any external state. So e.g. that serialization/deserialization service mentioned only takes a String as input, and returns a reference to a base-class object as an output (or the other way around for serialization). Making code conform to functional style makes it not only easier to test, it helps avoid some tests entirely.
This is usually where I draw the line for what to test. I don't care how a unit does what it does, I only care about what it does.
In my opinion a unit should have minimum side effects, none if possible, and the results should depend on the inputs. Write the test to the contract of what the caller should provide and caller will get back (and then any side that had to happening, like checking logs.)
It is almost as if they see the success that we have had in dev/ops with the "cattle, not pets" philosophy, and want to apply that in their own field. Making the subject behave consistently and predictably, whether it is a machine or a human professional, would be a prerequisite for that.
Sure, the intention is good, but it promotes mindless repeating of patterns over thinking about what really helps.
Our productivity is way up and our failure rates haven't changed. It's increased our time spent debugging, but not by as much as we had estimated that it would.
I won't pretend that's a good decision for everyone. But I do think people take test-driven-development a little too religiously and often forget to ask themselves why they are writing a certain unit test.
I mean, before I was a manager I was a developer and I also went to a university where a professor once told me I had to unit test everything. But then, another professor told me to always use the singleton pattern. These days I view both statements as equally false.
In my experience, unit tests don't catch many bugs when the code is fresh. But when it's five years old with many modifications over the code base, some dumb little test that you thought was a waste of time is now alerting you to what would have been a horror regression.
In other words: Even though it feels like it's slowing you down now, if you write tests while the functionality is still fresh in your mind, and you capture your assertions well, they'll pay off dividends in the future once you've forgotten what some block of code does.
This gives us some unique abilities in terms of modeling our productivity of course, because we started measuring before anyone thought up unit testing.
Over the past 15 years, unit testing has failed to produce anything positive, and test driven development has been an absolute disaster.
That being said, this isn't something which will be universally true. A lot of the software we use isn't build by us, and I'm certainly that a lot of the suppliers on that software use unit tests quite extensively.
For the things we build ourselves, however, there has been almost no value in adopting modern test philosophies.
You say that systems have to be able to be worked on 5 years from now, but the truth is that most of our systems transport data and rarely live 5 years without getting rewritten to deliver better performance, higher levels of security or simply because the business has changed completely. A lot of it hold very few responsibilities as well, making it extremely obvious what to fix when a service breaks.
Don't get me wrong, we've seen problems we wouldn't have with 100% coverage. But that doesn't matter when spending resources fixing them is still a net positive on every account.
> You don't need to test that. [...] The code is obvious. There are no conditionals, no loops, no transformations, nothing. The code is just a little bit of plain old glue code.
The code invokes a user-passed callback to register another callback and specifies some internal logic if that callback is invoked. I personally don't find that obvious at all.
Others may find it obvious. That's why I think, if you start with the notion "this is necessary to test, that isn't", you need to define some objective criteria when things should be tested. Relying on your own gut feeling (or expecting that everyone else magically has the same gut feeling) is not a good strategy.
If I rewrite some java code from vanilla loops-with-conditionals into a stream/filter/map/collect chain, that might make it more obvious, but it wouldn't suddenly remove the need to test it, would it?
>"But without a test, anybody can come, make a change and break the code!"
>"Look, if that imaginary evil/clueless developer comes and breaks that simple code, what do you think he will do if a related unit test breaks? He will just delete it."
You could make that argument against any kind of automated test. So should we get rid of all kinds of testing?
Besides, the argument doesn't even make sense. No one is using tests as a security feature against "evil" developers (I hope). (One of) the points of tests is to be a safeguard for anyone (including yourself) who might change the code in the future and might not be aware of all the implications of that change. In that scenario, it's very likely you change the code but will have a good look at the failed test before deciding what to do.
To see how 100% coverage testing can lead to great results, have a look at the SQLite project [1].
In my experience, getting to 100% takes a bit of effort. But once you get there it has the advantage that you have a big incentive to keep it there. There is no way to rationalise that a new function doesn't need testing, because that would mess up the coverage. Going from 85% to 84% coverage is much easier to rationalise.
And of course 100% coverage doesn't mean that there are no bugs, but x% coverage means that 100-x% of the code is not even run by the tests. Do you really want your users to be the first ones to execute the code?
As an anecdote, in one project where I set the goal of 100% coverage, there was a bug in literally the last uncovered statement before getting to 100%.
I mean, SQLite is a good example of something where 100% coverage would actually be useful, because it tries to maintain compatibility with the SQL spec and with Postgres (largely because Postgres complies with the spec). Testing that 100% makes a lot of sense.
Suppose you're instead doing something very UX/UI driven. Why bother trying to cover that 100% with automated tests, when change is going to be driven by the whim and fancy of anyone who sits down in front of it?
Yes, absolutely.
> Suppose you're instead doing something very UX/UI driven. Why bother trying to cover that 100% with automated tests, when change is going to be driven by the whim and fancy of anyone who sits down in front of it?
It might make perfect sense from a business standpoint to have no tests at all.
Some of my day involves writing Ruby. I find using Ruby without 100% code coverage to be like handling a loaded gun: I can track many outages to things as silly as a typo in an error handling branch that went untested. A single execution isn't even enough for me: I need a whole lot of testing on most of the code to be comfortable.
When I write Scala at work instead, I test algorithms, but a big percentage of my code is untested, and it all feels fine, because while not every piece of code that compiles works, the kind of bugs that I worry about are far smaller, especially if my code is type heavy, instead of building Map[String,Map[String,Int]] or anything like that. 100% code coverage in Scala rarely feels as valuable as in Ruby.
Also different styles make the value of having tests as a way to try to force good factoring changes by language and paradigm. Most functional Scala doesn't really need redesigning to make it easy to test: Functions without side effects are easy, and are easier to refactor. A deep Ruby inheritance tree with some unnecessary monkey patching just demands testing in comparison, and writing the tests themselves forces better design.
The author's code is Java, and there 95% of the reason for testing that isn't purely based on business requirements comes from runtime dependency injection systems that want you to put mutability everywhere. Those are reasons why 100% code coverage can still sell in a Java shop (I sure worked in some that used too many of the frameworks popular in the 00s), but in practice, there's many cases where the cost of the test is higher than the possible reward.
So if you ask me, whether 100% code coverage is a good idea or not depends a whole lot on your other tooling, and I think we should be moving towards situations where we want to write fewer tests.
I don't think I could write low-bug count code faster in statically typed scala than I could in unit tested ruby though. I mean I am well aware of what you're telling me, and it's obvious to me that having the compiler automatically check certain properties is a win. And yet, when push comes to shove, to get something done I'm more likely to reach for ruby.
It's something I've never come up with a good explanation for. Does static typing stunt prototyping and exploration? Do unit tests capture high level goals better?
I guess I don't agree with the assertion that ruby tests are useless. Because you test higher level things than you do with scala types.
Not in my experience. It is probably about what you personally find hard/uncomfortable/unfamiliar.
I've used static typing much more than dynamic. I'll admit confusion on things like ocaml polymorphic variants and haskell monads, nevertheless I wouldn't say I find static typing hard as a rule. But surely the point of static type checking is to constrict what you can do for safety and performance reasons. And surely the cost of that is you have less freedom - even when in the exploration phase
No. I didn't mean to imply or assert anything of the sort. Apologies if I inadvertently gave offense. You seem to be reading meanings into my reply that aren't there.
I don't know you from Adam. You asked a question in your post. I answered as best as I could.
That said, your latest statement
"I'll admit confusion on things like ocaml polymorphic variants and haskell monads,"
does seems to imply that you don't have much real world experience with static type systems in production (nothing wrong with that) since neither is an arcane concept or particularly difficult to understand.
If you haven't worked extensively with Haskell/Ocaml/SML etc, and you are extrapolating properties of 'static type systems' from those of Java or C++ then your idea of such type systems 'stunt prototyping and exploration' might make sense.
The rest of your comments are extrapolations from misunderstandings - not born of practical experience. My answer was based on (strictly) personal experience. Which is why I said "in my experience". I gladly concede that YMMV.
again, I was just answering your question in your original comment. I didn't mean to "imply" anything and used "probably" to mark my uncertainty about your real world experience with Ocaml/Haskell etc style static type systems.
I have extensive experience with both dynamically typed languages (mostly Python, Lua and Scheme) and statically typed languages (mostly Haskell and SML, besides Java). I answered out of my experience.
YMMV. And that is cool.
You're right, I haven't used ocaml or haskell in production. I did use F# though, which seems to be in the same ballpark as those languages in terms of having algebraic data types and inference and all the rest. I suppose the fact that I still don't really grok polymorphic variants after reading the real world ocaml a few times may say something about my ability, motivation, or at the very least how my brain is wired.
Fundamentally though, a type system is a restriction meant to help the programmer. This restriction must inevitably put you down a certain road when you explore stuff, right?
I'm going to be the one to point at the elephant in the room and say: Java. More precisely, Java's culture. If you ask developers who have been assimilated into a culture of slavish bureaucratic-red-tape adherence to "best practices" and extreme problem-decomposition to step back and ask themselves whether what they're doing makes sense, what else would you expect? These people have been taught --- or perhaps indoctrinated --- that such mindless rule-following is the norm, and to think only about the immediate tiny piece of the whole problem. To ask any more of them is like asking an ostrich to fly.
The method names in the second example are rather WTF-inducing too, but to someone who has only ever been exposed to code like that, it would probably seem normal. (I counted one of them at ~100 characters. It reminds me of http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom... )
Many years ago I briefly worked with enterprise Java, and found this sort of stifling, anti-intellectual atmosphere entirely unbearable.
The purpose of unit tests is to document the intended behaviour of a unit/component (which is not necessarily a single function/method in isolation) in such a way that if someone comes along and makes a change that alters specified behaviour, they are aware that they have done so and prevented from shipping that change unless they consciously alter that specification.
And, if you are doing TDD, as a code structure/design aid. But that is tangential to the article.
More like "in countless articles, comments, talks and projects over, say, the last decade".
Unit tests are typically inductive. Developer shows case A, B and C give the expected results for function f. God help us if our expectations are wrong. So, you're saying since A, B and C are correct therefore function f is correct. Well that may be, or maybe A, B and C are trivial cases, in other words, you've made a weak argument.
100% test coverage sounds like lazy management. Alas, the manager may have worked their way via social programming rather than computer programming. In such cases, better to say you have 110% test coverage.
double f( double x ) { return 1/ x; }
They pointed out that it is trivial to get 100% coverage in test cases but unless your tests include passing in 0 as the parameter you are going to miss an error case.
Mix this with unchecked exceptions, mocking of any real-world interaction that will generate errors in productions, and testing becomes a cargo-cult of quality.
Not arguing the point here. It's just a terrible example.
There is some other more abstract concept related to the classes of input that can possibly be passed to a method. IMHO 100% coverage and "test first" have done more harm than good to the cause of automated testing.
Of course I made the above up on the spot, but it is a reasonable thing to do for some situations. Those who know floating point math are well aware that dividing by something close to zero tends to result in very large errors, vs the true answer. (particularly if the division is part of a larger calculation)
I've been programming for a living since 1996, and only recently started to do TDD in the normal sense of writing unit tests before writing code. I've found to it to be an enormous help with keeping my code simple - the tests or the mocking getting difficult is a great indicator that my code can be simplified or generalised somehow
I argued for functional instead of unit testing for years, but it was only when a team-mate convinced me to try unit testing (and writing the tests FIRST) that the scales fell from my eyes. Unit testing isn't really testing, it's a tool for writing better code.
BTW from an operational perspective I've found it's most effective to insist on 100% coverage, but to use annotations to tell the code coverage tool to ignore stuff the team has actively decided not to test - much easier to pick up the uncovered stuff in code review and come to an agreement on whether it's ok to ignore
Once TDD "clicked," the quality of my code shot up very rapidly indeed.
I have seen quite a few codebases with loads of unit tests that might once have been useful, but now were just slowing down development. Many of them should have been thrown away and replaced by a couple of functional tests, however the sunk cost fallacy always kicked in, nobody wanted to throw away all the hours of work that had gone into making them and not to forget the precious code coverage as well.
This is a REALLY common developer attribute that drives me nuts - people don't want to throw away stuff that they put a lot of effort into.
The reason for this question is the naming mistake that was made in the beginning. They are not TESTS, they are SPECIFICATIONS.
Once your code has been written, are the specifications still useful? Well, duh... unless the business has changed them then yes, they're still useful.
To me, it's secondary and has tones of bike shedding in it. Writing tests is easy, mentally. Getting a good, simple, YAGNI/DRY architecture is more challenging and requires several iterations, something that is resisted if you have to rewrite all your unit tests. Let me put it this way, if you write an architecture that you later don't like, you would be more hesitant to scrap it and start over because all the additional work of the unit tests (especially if you're on a deadline). That's bad. To write a good architecture, a developer must be willing to realize he could have done it better and be willing to tear it down instead of build around the bad architecture. That's how you write 20 year code, which should be almost everyone's goal.
But the developer response in a couple cases was to puff the code up with layers of fluff that just added levels of abstraction that just passed stuff down to the next layer, unchanged, with a bunch of parameter checking at each new level. This had the effect of adding a bunch of code with no chance of failure, artificially increasing the amount of code covered by the tests (which, by the way, were bullshit).
I got to rip all that junk out. It ran faster, was easier to understand and maintain, and I made sure I never, ever worked with the people who wrote that stuff.
cat /dev/random | ./build-inline.sh | ./test-inline.sh | tee ./src/blob.c &&
git commit -Am "I have no idea how this works, but I am certain that it works perfectly, see you all on Monday!" &&
git push production master --force
When presented like this, relying on human intelligence and experience doesn't seem like such a bad thing after all.Just so we're clear, my username was not inspired by this scheme.
This is known as mutation testing.
https://en.wikipedia.org/wiki/Mutation_testing
I've tried it a few times, I generally found it too slow to be an everyday part of my routine, but interesting tool to have in your belt for e.g. evaluating test in a codebase you didn't write yourself.
If you have, say, 95% coverage -- and most corporate dev orgs would be thrilled with that number -- and then you commit some new code (with tests) and are still at 95%, you don't know anything about your new code's coverage until you dig into the coverage report. Because your changes could have had 100% coverage of your new thing but masked a path that was previously tested; or had 10% but exercised some of the previously missing 5%.
If you have 100% coverage and you stay at 100% then you know the coverage of your new code: it's 100%. Among other things this lets you use a fall in coverage as a trigger: to block a merge, to go read a coverage report, whatever you think it warrants.
Also, as has been noted elsewhere, anything other than a 100% goal means somebody decides what's "worth" testing... and then you have either unpredictable behavior (what's obvious to whom?) or a set of policies about it, which can quickly become more onerous than a goal of 100%.
It's important to remember that the 100% goal isn't going to save you from bad tests or bad code. It's possible to cheat on the testing as well, and tests need code review too. There's no magic bullet, you still need people who care about their work.
I realize this might not work everywhere, but what I shoot for is 100% coverage using only the public API, with heavy use of mock classes and objects for anything not directly under test and/or not stable in real life. If we can't exercise the code through the public API's then it usually turns out we either didn't rig up the tests right, or the code itself is poorly designed. Fixing either or both is always a good thing.
I don't always hit the 100% goal, especially with legacy code. But it remains the goal, and I haven't seen any convincing arguments against it yet.
Open the flame bay doors, Hal... :-)
If you know what's going on -- and hopefully you do if you're making decisions like whether to pursue a 100% coverage goal -- then your point is (I hope) self-evident.
It's also why code review is necessary and not just the coverage metric. 100% coverage is not "100% of all things that could happen," it's "100% of all code execution paths" and (hopefully) a sample of all known types of valid and invalid inputs.
Anyway, so far I've found that most developers respond pretty well to the 100% idea if you explain its utility and what it does and doesn't get you. From a ridiculously small sample, granted.
The utopian risk, I think, is much more around the hope that people high up the food chain in your organization actually understand this stuff. Consider how many VPs of Engineering did a year or two of engineering work (before unit testing was in vogue) then got that MBA and have been busy taking meetings ever since.
Here's the code:
@Override
public void initialize(WatchlistDao watchlistDao) {
watchlistDao.loadAll(watchListRow -> watchlists.add(watchListRow));
}
Maybe I'm dense, but this code raises at least one question that I would prefer to see answered by tests.The parameter watchlists appears to be defined in a scope above the one under test. What happens if watchlists is null for some reason? What should be the behavior?
Then there's the tricky question of what to do as this method evolves. Next month, a watchListRow might need to be updated with a value before being added to watchlists. Later, a check might be added to ensure some property exists on watchListRow. At what point will a test be written for this method?
Some feedback on the article:
>Test-driven development, or as it used to be called: test-first approach
Test-first is not the same as Test-Driven. The test-first approach includes situations where a QA dev writes 20 tests, and then hands them to an engineer who implements them. Thats not TDD.
>"But my boss expects me to write test for all classes," he replied.
That's very unlikely to be TDD. "Writing tests because I've been told to" is never likely to be "I'm writing the tests that I know to be necessary", and that's all TDD is: writing necessary tests. If the test isn't necessary, then neither is the code.
>Look, if that imaginary evil/clueless developer comes and breaks that simple code, what do you think he will do if a related unit test breaks? He will just delete it.
Sure. But then their name is on that act in the commit log. The test is a warning. I've been lucky not to have worked with evil developers, but I have worked with some clueless ones, and indeed some have just deleted tests. Thats an opportunity for education, and quality has steadily improved.
>The tragedy is that once a "good practice" becomes mainstream we seem to forget how it came to be, what its benefits are, and most importantly, what the cost of using it is.
Totally agree. So many programmers and teams practice cargo cult behaviors. Unfortunately, this article is one of them: making claims about TDD, and unit tests in general, without understanding "why" TDD is effective.
How did you come to this conclusion?
What if the code is just glue code for different libraries?
But in my experience, code covering unit tests have correlated strongly with faster development and far fewer bugs being uncovered in the field.
If the metric actually measures the figure of merit rather than a proxy with no necessary linkage, this is not true; working the metric then is working the intent of the metric.
The extent to which a metric can be "worked" distinct from its intent is exactly the extent to which it is measuring something disconnected from what it is used to assess.
Always write tests. And strive for maxmium coverage. But make sure you write the right kinds of tests:
- Don't overuse mocks. Mocks don't represent real conditions you would actually see in production. Favor using real dependencies over mocks.
- Don't overspecify your tests. Test only publicly specified parts of the contract. Things that you need to be true and that the callers of the module expect to be true. And yes, you will change the test when the contract changes.
I advocate spending time on identifying/inventing the correct abstractions over coverage.
Naming the test just "initialise" is not very useful as it doesn't assert what you expect the method under test to do. Given that the purpose of the initialise function is to populate a watchlists collection variable from the parameter, i'd name the test something like "initialise_daoRecordCountIs9_watchlistCountIs9". The pattern I generally use is <method_name>_<assertion_under_test>_<expected_result>.
Then, my test would be the following:
* Set up / mock the dao parameter to have 9 rows
* Create an instance of the class under test and push in the dao parameter
* Verify / Assert that the class under test now has 9 items in the watchlists variable - I'm assuming there is a public method to access that.
Do it for the high level bits you actually expose. If you're exposing everything, tests won't really save you - architecture and modularity are more fundamental and should be tackled first. If you're writing a big ball of mud, what benefit do you get testing a mudball?
Edit: while I'm a huge TDD advocate, I'm not a big advocate of measuring code coverage. That should only be necessary if you are trying to get a code base under coverage that wasn't TDD'd. Even then I'd rather add the coverage as I'm touching uncovered code. If it works and I'm not touching it, it doesn't need tests.
100% coverage for my team means that we were intentional about our code. It's not hard at all to have 100% coverage in a Ruby application as it is possible to do a lot with very little code.
Furthermore it allows us to bring in a junior on the team because we know they have a safety net.
Also for the record we do code reviews and are very thoughtful about the code we write. 100% coverage does not stop the possibility of some bugs inserting themselves somewhere.
TDD just means that when discover the bug and add the missing try/catch you don't accidentally break something else.
1. Is it hard to instantly test the code when implementing it? (Might be the case for library code)
2. Is there a chance the underlying implementation might change (and so might break in the future)?
3. Will the interface of the class remain stable? (If not unit test needs to be rewritten too)
4. Will functional tests pass when something breaks in this class?
Testable code tends to be better (have less dependencies, less state etc) than non testable code. So making it testable is a goal in itself, even if the tests are never even committed.
There's a good chance that the dev time needed to go from 90% coverage to 100% coverage might be better spent somewhere else.
Writing tests to confirm the simplest, most predictable use cases is a waste of time - Those cases can be figured out very quickly without automated testing because they are trivial to reproduce manually.
In hardware verification (where I come from, and where the cost of bugs is usually higher), "functional coverage" is considered more important. This is usually achieved via constraint-based randomization (somewhat similar in spirit to QuickCheck, already mentioned in this thread).
I tried to cover (ahem) this whole how-to-use-and-improve-coverage topic in the following post: https://blog.foretellix.com/2016/12/23/verification-coverage...
As with much else TDD should be a tool with the ultimate goal of aiding us in writing correct and less bug riddled code, once the tool adds more work it's no longer offering much aid.
In my opinion the best advice is to force developers to use their brains. I know, there are a lot of sh*tty CTO/CEO/HoIT/SomeOther"Important"Position people out there seeing them as code monkeys and saying that developers are not paid to think but in that case the best thing developers could do is learn to say "NO"... My experience with that kind of people is that they need to learn the meaning of "NO" instead of wasting time and money in the end of the day.
IntelliJ has a plugin
http://www.ncover.com/blog/change-risk-anti-patterns-code-co...
"This is based on the methodology that the more complex code is the more likely it is to have errors, hence the need for greater code coverage.
To better understand, let’s assume a scenario where you wanted to keep your change risk anti-patterns score below 30. To maintain this level with a code set that has a cyclomatic complexity of 10, you would need to achieve a code coverage (as defined by branch coverage) ratio of approximately 42%. If however, the complexity of your code was greater and you had a cyclomatic complexity of 20, you would need almost 72% code coverage if you wanted to maintain the same risk level.
Complexity increases risk. Testing decreases risk. The change risk anti-patterns score give you a metric to measure the correlation between the two."
Which he also gave as an answer here:
http://stackoverflow.com/questions/153234/how-deep-are-your-...
But I know a lot of people in the early days of XP went to extremes, 100% code coverage, mutation tools for every condition to ensure unit tests broke in expected ways, etc. But they were more experiments in pushing the limits rather than things that gave productivity gains.
Behaviour is completely covered by tests, so changes in APIs which might break consumers of the library will at least be detected.
New work on the library tends to follow the 100% coverage by convention, so it's somewhat easier to maintain. Apps that have 90% coverage, for example, tend to slip and slide around. Having 100% coverage projects the standard "If your contribution doesn't have 100% coverage it won't be accepted". I don't think this is a bad default position.
So if I want to contribute to your project all I have to do is write some pointless tests that are sure to execute every single getter and setter method.(yes I have seen tests that exist solely to execute getters and setters). I don't have to actually test any known edge cases.
Or one I've been personally hating lately, a nil pointer dereference in Golang from a well tested library that didn't do proper error propagation.
Yes. Code coverage should not be the primary measure of quality of your tests. That thinking leads to tests designed to cover lines, not use cases.
This is true.
But isn't that true of any test? A suite of naive or badly written tests can also give us a false sense of security, so why write any at all (I don't mean that literally)?
I think that a greater level of confidence in our code can be achieved by a combination of
- Judicious choices of unit and integration tests
- Static and dynamic analysis (if the language supports it), and
- Property-based testing (the canonical example of which is QuickCheck[1]). Property-based testing is a great way to help us hit those edge cases.
As for 100% code coverage, I think it is worth striving for, not just for the sake of having all lines of code tested. It can expose design and testability flaws, for example.
If 100% coverage cannot be achieved, we need to ask ourselves:
- Did we really need that piece of code we couldn't test? Is it actually called anywhere, or is it one of those YAGNI things?
- Did we write code that is not very testable? Are there functions or methods, for example, that are so dependent on external state that they can't be mocked or tested some other way? Should we refactor it?
- If it's a trivial line of code like a getter or setter that cannot possibly be wrong, then it should be fairly trivial to generate automatically a test case for it. More severe defects have probably been caused by a single line of untested code[2][3] than we may suspect.
- If it's a getter or setter, and it's not used (and therefore code covered) in other test cases, maybe it's superfluous and should be removed.
References
[1]: https://en.m.wikipedia.org/wiki/QuickCheck
[2]: https://www.imperialviolet.org/2014/02/22/applebug.html
[3]: http://users.csc.calpoly.edu/~jdalbey/SWE/Papers/att_collaps...
More importantly, if it's a getter or setter, the actual behaviour to be tested is probably broader than "should set/get x."
Test scenarios, not methods.
Let someone else look over your code, they might spot logic bugs that you oversee. If you write tests yourself, you will often oversee such logical bugs.
I have done (semi-)automatic reasoning and code verification with 100% code coverage of OS drivers. All the mathematical reasoning doesn't spot logical errors. You need more than one person to look over it.
People of that school tend to write tests that test implementation rather than functionality. As a result you get fragile tests that break not telling you what went wrong but how the implementation has changed.
Good tests should test behavior. A change in implementation shouldn't break the test.
Even though Uncle Bob's advice agrees with this submission:
https://news.ycombinator.com/item?id=14301466
>People of that school tend to write tests that test implementation rather than functionality. As a result you get fragile tests that break not telling you what went wrong but how the implementation has changed.
Also another thing Uncle Bob does not advocate. From his stance, if most of your failing tests are due to refactors (changing implementation), and not bugs, you need to redesign your tests. Abstract out your interfaces so tests are not too dependent on the actual implementation. Tests are first class code, not to be treated differently from your main code base - they should abide by code standards, and they need to be architected as much as your regular code.
(Easier said than done).
The thing is, I'm not even an Uncle Bob fan. But it's crazy how neither his fans nor his detractors see the nuances in what he says - be it his videos or his blog posts.
What is it about programmer types that makes them see everything in binary?
Escape a `while True` loop? Log it, along with the current state of the program, and blow up (so we can be restarted). Memory allocation error? Log it. The big "unexpected exception" clause around my main function? Log it.
If I do hit those in testing, my code is wrong.
Codebases serve businesses and businesses value legibility over efficacy. It's more important to them to have control over their assets than to have better assets. Using one test framework is in perfect service of that goal.
It's inefficient in that it will take future developers more time to understand that code. But fewer architectural elements means that you can get by with less senior programmers.
Imagine if you went onto a software project and they were using 6 different databases because every time they had a new kind of data that they wanted to access differently, they reached for another database rather than use the one they had.
Of course nobody would ever do that, well I hope anyway, but I do see a lot of unnecessary architectural complication in projects in service of "using the right tool for the job." And it can balloon. A new test framework has to work in your CI framework. You need to decide how to handle data. It's not a huge decision, but it's more complicated then most devs would think and it'll take up more of your time than you'll expect.
You can generalize this to the the main thrust of the article. 100% code coverage is not a bad goal to want to hit. Sure, you're going to get a lot of waste. But you're not paying for it, your employer is. And your employer might have a different idea of which side of the tradeoff he wants to be on and where to draw the line. You know the code way better than they will, but they know the economics far better than you ever could.
That will give you good coverage, while avoiding too simple to be useful unit tests.
It's really hard to foresee all possible input variations and business logic validations, but that doesn't mean your test suite is useless.
It just means it will grow everytime you find a new bug and you are guaranteed that one won't happen again...
Write a test if you don't feel confident that a piece of code does what you think it does. If you're not sure what it does now, there's little chance that you or anyone else will in the future, so write a test to understand it and to make that understanding explicit.
Use curiosity as a driver.
Automated tests are a spec, and are exactly as hard to write completely and correctly, and as easy to get wrong in ignorance, as a 'real' spec. If you find them easy to write, odds are good you would find the code easy to visually verify as well - which is to say, you're working on a trivial problem.
They have their place, but that place is not everywhere. It is where they are efficient and valuable. I particularly look for places where they are like the P half of an NP problem, an independent estimate of the answer to a math problem. If you ever find yourself writing the same code twice, unless it's a safety-critical system or something, that's a moment to stop and reflect on the value of what you are doing.
100% coverage on project of which size? Imagine you have a single script project that does exactly one thing and 2 test are enough to verify that it works without doing it manually? That is not the same as writing test for file system or tests which consists mostly of mocks upon mocks upon mocks.
I think the real problem is someone comes up with an idea, like TDD, tells people about it, some people hear about, start preaching it, some people start believing it and nobody actually think things through, usually cos they don't have experience (it's not a fetish as someone said). Like everything in life, you have to think things through before doing them, ask your self is this worth doing and when it is worth doing. You can't just say: "Oh we are doing TDD thus everything must be done in TDD way".
For people that say tests are useless, or good code does not need tests, I ask, when you make a change do you still make sure your code works by hand? And if you do make sure, why don't you automate that? You are a programmer after all.
And for those that say you need to test everything, well you don't, specially if you need mock most of it or it is really not that important piece of code as it is dev tool or something. What you want to make sure works is customer/user facing stuff that must work for you to get paid and you want to be able to verify this at any time of day without losing hours clicking around checking for stuff.
So this is not straight forward, 100% means nothing without context and doing anything in excess and without valid reasons is pointless or even harmful. And this has nothing to do with programming but life in general.
Not to mention 100% coverage is not guarantee the system works, in practice quite the opposite
Not to mention this BDD crap which only makes my blood boil, it's syntactic yuck disguised as syntactic sugar
I came here just to search for Uncle Bob to see the fun comments talking about him.
No, he does not recommend 100% coverage.
He is against writing tests for "basic" code like getters/setters.
I can understand the irritation with Uncle Bob - especially if you've read his blog. But everyone seems to get his stance on unit tests wrong - including Uncle Bob fans.
His fans say he advocates for 100% code coverage.
His detractors say he advocates for 100% code coverage.
All while Uncle Bob is saying he doesn't cover "trivial" code.
Initial "complete coverage" should probably start from mockups that test an entire API. The complete part should be that, in some way, the tests cover expected successes AND failures (successfully return failure) of every part of the API, but there's no need to test things individually if they've already been tested by other test cases.
Invariably reality will come up with more cases and someone will notice an area that wasn't quite fully tested. That's where a bug exists, but the golden test cases probably wouldn't have located it anyway. It'll take thousands or millions of users to hit that combination and notice it. Then you get to add another test case while you're fixing the problem.
It's good to use hyperbole black and white when teaching so the point comes across easier. But they should be made aware of caveats before they graduate at least.
It's not a silver bullet though. Some property-based tests are easy to write but offer little value. Sometimes you spend more time writing code to generate the correct inputs than the value of the test warrants. It has a learning curve. Still, I think it is the most powerful tool you can master for testing.
I rewrote the test by converting the constant to a string, taking a checksum of it, and comparing _that_ to a short hardcoded value. Now the test is just 1 line of code, instead of 41! Then I put it through code review, and my team said "What a ridiculous test." But they didn't see any problem in the previous version that compared it to a 40-line hardcoded hash.
It's a weird world.
How I do it is going from rough testing of pages and components to granular testing of those parts which had some error.
For pages, I just run them to see if they display without producing errors, same goes for critical components. This gets me the feeling of roughly tested and from the user perspective working system with little time investment.
Then I test critical business logic, but usually only after some error was reported.
Mind though that I am freelance developer unconstrained by organizational rules.
For instance, I remember all the bad code that I wrote and read circa 1997-1999, after design patterns became the rage.
The danger of 100% percent coverage is that the goal of tests becomes the 100% code coverage and not bug detection anymore.
When I have something that should be working, I test it in a more functional/integrative manner, and move on.
Later, I'll write unit tests when I need to. If I want to refactor something, or drastically change the implementation of something, I'll write out some tests beforehand to be sure that the pre and post behaviors match.
I've always thought that TDD is just premature optimization. You're optimizing for the idea that you -- or someone -- will later need to make large enough changes to your code that you'd worry about breaking it. In my experience that's fairly rare, and you spend less time overall if you just write the tests as you need them, not up-front. Yes, writing a test when the code is fresh in your mind will be faster than writing it much later, but then you're writing a ton of test code that likely won't be necessary.
An objection I hear to this is that you're not just writing tests for yourself, you're writing tests for the others who will need to help maintain your code, perhaps after you're gone. I'm somewhat sympathetic to this, but I would also say that if someone else needs to modify my code, they damn well better first understand it well enough such that they could write tests before changing it (if they deem it necessary). Anything else is just irresponsible.
(Note that I primarily work in strongly statically typed languages. If I were writing anything of complexity in ruby/python/JS/etc., I don't think I'd feel comfortable without testing a lot of things I'd consider trivial in other languages.)
(Also note that some things are just different: if you're writing a crypto library, then you absolutely need to write tests to verify behaviors, in part because you're building something that must conform to a formal spec, or else it's less than worthless.)
As someone who has had to fix plenty of legacy code, I have truly appreciated the people who have left me at least some working test suites to run - or just look at - and cursed many others. At the same time, I have generally been handed over code bases with tens or hundreds of thousands of lines, some of which had no useful tests.
If it is irresponsible to try to refactor or fix a codebase without first understanding all of it, it may be even more irresponsible to expect that those who follow in our footsteps will be able to do that, even if they are "gods of programming".
The reason why this is so was hammered home very strongly in Peter Naur's "Programming as Theory Building"[1].
Unless a code base is trivially small or simple, leaving it without meaningful tests instantly creates legacy code. I'll close with an excerpt from the back cover of "Working Effectively with Legacy Code[2].
> Is your code easy to change? Can you get nearly instantaneous feedback when you do change it? Do you understand it? If the answer to any of these questions is no, you have legacy code, and it is draining time and money away from your development efforts.
[1]: https://news.ycombinator.com/item?id=10833278
[2]: https://www.amazon.com/Working-Effectively-Legacy-Robert-Mar...
Regarding your "legacy code" quotes: I don't write code that isn't easy to change, or easy to read. If I write code that's hard to read, I delete it and start again. If I absolutely cannot write something that's easy to read, I write tests around it and document the hell out of it (I'm iffy on comments and docs as well, because they _always_ end up out of date, and then are more of a hindrance than no comments at all).
I think I'm pretty good at what I do, but I wouldn't consider myself a "god of programming". Writing clear, concise code isn't hard. It really really really isn't. In my experience, the main blocker to that is ridiculous time lines and pressure to ship. I know it can be hard to push back against that pressure, but you owe it to yourself, future maintainers of your code, and the company you work for (even if the company doesn't realize or appreciate it at the time) to slow things down and do things the right way. A former colleague used to say, "It's not right because it works; it works because it's right". Just because a bit of code produces the output you want, it doesn't mean it's right. Write the right code -- readable, maintainable, verifiable, testable -- and you don't even need to worry about it working, because of course it will.
I lean on type systems heavily. If I were writing python or ruby, I'd have 10x as much test code as application code, because I just do not believe you can trust a dynamic/weakly-typed language without them. This is why I avoid such languages; I think any gains in rapid development that you get from such languages are quickly wiped out by the need to write extensive tests, or, lacking those, all the bugs that come up because you don't have them.
These days I write most things in scala, if I can. No, it's not a perfect language (honestly, I'd say a half to two thirds of it is crap, but the rest of it is amazing), but it has a strong type system that lets you lean on the compiler so much more than many other languages. Simply the fact that it compiles gives me much higher confidence than with most other languages.
I much prefer to use code coverage as a weak indicator for finding dead code.
Totally agree. You can say this about lots of things really and not just tests.
So
``` Int => add(x, y) => x + y; ```
Doesn't get a test, however
``` Int => formulateIt(x, y) => (x * y)^y ```
Does
Think about one question first: why did the manager force develop to achieve 100% coverage? There must have some benefits, or the manager might come from the competitor. When standing at a higher position, think of time and organization factors, it might be a good choice. If every engineer in the corporate has the deeply understanding of test coverage as the author, they really do not need to pursue 100% coverage. But in reality, we can see many companies which do not pursue test coverage, their coverage tend to be 0. That's why we need force 100% test coverage in a short time. Engineers need time to form the habit of test their code, and then experience the pain of bad tests. Then they start to think what kind of tests are valuable.
https://kev.inburke.com/kevin/the-best-ways-to-find-bugs-in-...
Working smarter isn't always in opposition to working harder.
It is funny how things turn around. For fifteen years I
have been preaching TDD (Test-driven development, or as it
used to be called: test-first approach), or at least for
developers to write some unit tests. However, in recent
times I have found myself saying more often, "Why did you
write that test?" instead of, "You should write a test."
Answer to that "funny turn around" sentence is simple - he is getting older and more lazy. Each of us will, each of us should fight with it.And after that, example of what I'm talking about:
It seems that he had trouble using Mockito to test the
following piece of code:
...
I think he was very surprised with my response: "You
don't need to test that."I'm not sure there's much use for it in other scenarios.