Writing Great Unit Tests: Why Bother?
Unit testing is the practice of testing the components of a
program automatically, using a test program to provide inputs to each component
and check the outputs. The tests are usually written by the same programmers as
the software being tested, either before or at the same time as the rest of the
software. What’s the difference between a good unit test and a bad one? How do
you learn how to write good unit tests? It’s far from obvious. Even if you’re a
brilliant coder with decades of experience, your existing knowledge and habits
won’t automatically lead you to write good unit tests, because it’s a different
kind of coding and most people start with unhelpful false assumptions about
what unit tests are supposed to achieve. It’s overwhelmingly easy to write bad
unit tests that add very little value to a project while inflating the cost of
code changes astronomically. Does that sound agile to you?
Unit testing is not about finding Defects
Unit tests, by definition, examine each unit of your code
separately. But when your application is run for real, all those units have to
work together, and the whole is more complex and subtle than the sum of its
independently-tested parts. At a high-level, unit testing refers to the practice
of testing certain functions and areas – or units – of our code. This gives us
the ability to verify that our functions work as expected. That is to say that
for any function and given a set of inputs, we can determine if the function is
returning the proper values and will gracefully handle failures during the
course of execution should invalid input be provided.
Ultimately, this helps us to identify failures in our
algorithms and/or logic to help improve the quality of the code that composes a
certain function. As you begin to write more and more tests, you end up
creating a suite of tests that you can run at any time during development to
continually verify the quality of your work.
A second advantage to approaching development from a unit
testing perspective is that you'll likely be writing code that is easy to test.
Since unit testing requires that your code be easily testable, it means that
your code must support this particular type of evaluation. As such, you're more
likely to have a higher number of smaller, more focused functions that provide
a single operation on a set of data rather than large functions performing a
number of different operations.
A third advantage for writing solid unit tests and
well-tested code is that you can prevent future changes from breaking
functionality. Since you're testing your code as you introduce your
functionality, you're going to begin developing a suite of test cases that can
be run each time you work on your logic. When a failure happens, you know that
you have something to address.
Well then, if unit testing isn’t about finding bugs, what is it about?
I bet you’ve heard the answer a hundred times already, but
since the testing misconception stubbornly hangs on in developers’ minds, per
principle. As Test plan development forum keep saying, “TDD is a design
process, not a testing process”. Let me elaborate: TDD is a robust way of
designing software components (“units”) interactively so that their behavior is
specified through unit tests. That’s all!
Tips for writing great unit tests
Enough theoretical discussion – time for some practical
advice. Here’s some guidance for writing unit tests that sit comfortably at
Sweet Spot on the preceding scale, and are virtuous in other ways too.
·
Make each
test orthogonal (i.e., independent) to all the others
Any given behavior should be specified
in one and only one test. Otherwise if you later change that behavior, you’ll
have to change multiple tests. The corollaries of this rule include
·
Test
passes but not testing the actual feature
Beware of such test cases making place in your code repository which
seems to doing lots of stuff in code, but in actual they were doing nothing.
They were sending requests to server and no matter what server respond, they
were passing. Horror!! They will become liability on you to carry them, build
them and running them every time but without adding any value.
·
Testing
irrelevant things
This one is another sign of bad test case. I have seen developers
checking multiple irrelevant things so that code passes with doing SOMETHING,
well not necessarily the correct thing. Best approach is to follow single
responsibility principle, which says, one unit test case should test only one
thing and that’s all.
·
Don’t
make unnecessary assertions
Which specific behavior are you
testing? It’s counterproductive to Assert () anything that’s also asserted by
another test: it just increases the frequency of pointless failures without
improving unit test coverage one bit. This also applies to unnecessary Verify
() calls – if it isn’t the core behavior under test, then stop making observations
about it! Sometimes, Testing folks express this by saying “have only one
logical assertion per test”. Remember, unit tests are a design specification of
how a certain behavior should work, not a list of observations of everything
the code happens to do.
·
Don't
ship code with tests that fail, even if it doesn't matter that they fail.
It's not uncommon, particularly in
test-driven development, to change your mind during design about which tests
are correct or relevant, or to make an initial implementation that only covers
some of the test suite. But that means you end up with failed tests that you
don't actually care about. Remove them, or at very least, document them: anyone
running your tests should be able to assume that a failed test indicates broken
code.
·
Consider
using a Code coverage tool to check how much of your code is actually being
tested. Coverage doesn't tell you everything: it only measures static
execution paths, but it can give you some idea of things you might have missed
altogether.
·
Whenever
you find a bug in “finished code”, add a test for it.
Make sure the test fails in the buggy
code and passes when it is fixed. Areas of code you've found bugs in are more
likely to be fragile in general, and bugs that have already been found are
relatively highly likely to reappear.
·
Test only
one code unit at a time
Your architecture must support testing
units (i.e., classes or very small groups of classes) independently, not all
chained together. Otherwise, you have lots of overlap between tests, so changes
to one unit can cascade outwards and cause failures everywhere.
If you can’t do this, then your
architecture is limiting your work’s quality – consider using Inversion of
Control.
·
Separate
out all external services and state
Otherwise, behavior in those external
services overlaps multiple tests, and state data means that different unit
tests can influence each other’s outcome. You’ve definitely taken a wrong turn
if you have to run your tests in a specific order, or if they only work when
your database or network connection is active. (By the way, sometimes your
architecture might mean your code touches static variables during unit tests.
Avoid this if you can, but if you can’t, at least make sure each test resets
the relevant statics to a known state before it runs.)
·
Avoid
unnecessary preconditions
Avoid having common setup code that
runs at the beginning of lots of unrelated tests. Otherwise, it’s unclear what
assumptions each test relies on, and indicates that you’re not testing just a
single unit. An exception: Sometimes I find it useful to have a common setup
method shared by a very small number of unit tests (a handful at the most) but
only if all those tests require all of those preconditions. This is related to
the context-specification unit testing pattern, but still risks getting
unmaintainable if you try to reuse the same setup code for a wide range of
tests.
·
Don’t unit-test
configuration settings
By
definition, your configuration settings aren’t part of any unit of code (that’s
why you extracted the setting out of your unit’s code). Even if you could write
a unit test that inspects your configuration, it merely forces you to specify
the same configuration in an additional redundant location.
Conclusion:
Without doubt, unit testing can significantly increase the
quality of your project. Many in our industry claim that any unit tests are
better than none, but I disagree: a test suite can be a great asset, or it can
be a great burden that contributes little. It depends on the quality of those
tests, which seems to be determined by how well its developers have understood
the goals and principles of unit testing.
This shows your in-depth knowledge in Programming skills. TDD is the best approach
ReplyDeleteGreat Stuff Buddy.... These are basic things we developer forget to follow. Thanks for reminding
ReplyDeleteSuper Like :):)
ReplyDelete