What is unit test anyway?
A unit test is a piece of code that takes one unit (usually a method or free function, sometimes an entire class or module), isolates it from the rest of the application, runs it against a known environment, and verifies that it behaves as expected. A typical pattern is "set up, define inputs, run, assert outputs, tear down", where the "set up" and "tear down" steps create the known environment. For any unit that depends on other units, a technique called "mocking" is used, that is, the tested unit is put in a context where the dependencies aren't the real thing, but rather placeholders / stubs. For example, if you test a method that runs against a database class, you don't use the real database class, but a mockup class that ignores its input and returns a hard-coded data set containing what the real one should return.
Ain't I testing a specific part of code while running the whole app itself?
You are, but not in isolation - you are testing the entire application, and when something fails, you often know that it fails, but not where and why. Unit tests tell you that each individual part of your machinery works as specified; testing the whole thing, or larger parts of it, in concert (a.k.a. "Integration testing", "End-to-end testing", or "Simulation testing") is just as important at unit testing, but it serves a slightly different purpose and has a slightly different focus.
What is its use?
Unit testing has several uses:
- Verification - unit tests should be written such that a passing unit test means that some aspect of the unit works as intended. If all aspects of the unit are covered by tests, you have 100% "coverage", and assuming that the tests accurately reflect the intended behavior, you can be confident that the unit as a whole works correctly.
- Troubleshooting/Debugging - when things go wrong, you will either have a failing unit test somewhere, which will allow you to pinpoint the problem, or all the tests passes, but things are still going wrong. In the latter case, existing unit tests will help you rule out possible causes and find spec mismatches, and if it turns out that you don't have the kind of coverage you thought you had (e.g., you missed some edge case), you can add a unit test that reproduces the problem, then fix it and use the unit test to verify (see above) that the fix is correct.
- Regression guards - every unit test you have guards against one possible regression, that is, a change in behavior that is caused as a side effect of another change. If you have good unit tests in place, and you make a change that breaks things elsewhere, your unit tests will tell you. Having complete test coverage will allow you to move faster and make more radical changes with higher confidence.
- Documentation - since a unit test explicitly tests for one behavioral aspect of a unit, it can serve as a form of documentation: a unit test says "if I use this unit here in such a context and pass it these inputs here, it will produce those outputs". This is valuable information for other developers, and better yet, because it is code that gets validated on a regular basis, it is practically guaranteed to be up-to-date at all times, unlike more traditional forms of technical documentation.
- Design - there is a whole development methodology (TDD, Test-Driven Design) built around the idea of using automated tests as an integral part of your design process. The mantra is "red, green, refactor": define the feature by writing a test (ideally, just one test) that captures the essence of your intended change; verify that it fails ("red"); change the code just enough to make the test pass ("green") without flipping any other tests to "red"; then refactor the codebase to remove duplication, redundancy and other code smells while still keeping all tests in "green". TDD forces you to work in a highly incremental way, it makes sure you always have near-perfect coverage, and it gets you to focus on one thing at a time. Particular challenges with TDD are that it takes some experience and vision to arrive at an elegant overall design with it (being religious about refactoring helps tons though), and that it doesn't work too well in a highly explorative context where you need to hack away at the code for a while before you even know what the problem is you're trying to solve.
I have read few articles, wikipedia etc. but I am not sure whether unit testing is a time consuming or a time saving process.
The jury is still out on this one, and rigid research in this area is hard if not impossible (how do you do double-blind studies about programming methodologies?) However, there seems to be quite some consensus that automated tests are an investment worth making for almost all projects; a single debugging session can easily eat up ten times the budget spent on writing unit tests, and an unnoticed design flaw can be many times more expensive even. Consider the alternatives:
- Deploy untested code. This is sometimes a valid choice, e.g. when the code is low-impact (say, a screensaver), or when moving fast is more important than delivering code that works well - in other words, when the risk of deploying a bug is smaller than the risk of not deploying in time. Usually, this is not the case though.
- Test manually. Manual labor is expensive, and it stays expensive every time you do it. As your codebase grows, so does the manual testing cost per deployment. To make matters worse, manual testing is repetitive work, something that humans are bad at - your testers will make mistakes, they'll raise false positives and miss bugs. Testing manually also takes a lot of time, which means your deployment procedure will take much longer - instead of a nightly build that runs ten thousand automated tests in a few hours, your testers go through a multi-day routine.
- Formal proof. While it is theoretically possible to do formal proofs of correctness for any given computer program, even very trivial ones are too complicated to do it by hand. This means we need a computer program to do the proving for us, and such tools exist - unfortunately, them being computer programs, they require us to feed them the specifications in a formal language. If you want a complete proof of correctness, you need to give the prover program the entire specification in a formal language - but wait, isn't programming all about encoding requirements in a formal language? Yes it is. So what you really do in the end is write the program twice, in two radically different languages, and then automatically check that the two implementations are equivalent.
- Rely on the compiler. Some programming languages have powerful type systems and compilers that can check them; in extreme cases, this rules out a large class of possible bugs, and you can leverage such a type system to give you quite some impressive guarantees about your code, for free. Still, you can't encode everything in a type system, so while a good type system can certainly remove the need for a whole truckload of trivial tests ("does this function fail when I pass it
null instead of an integer? does it fail when I pass it a string? an object of type
UserManager?"), but you will still have a lot of things to test for.