It is.
Even if you take just unit testing, it is not unusual to have more code within the tests than the actually tested code. There is nothing wrong with it.
Consider a simple code:
public void SayHello(string personName)
{
if (personName == null) throw new NullArgumentException("personName");
Console.WriteLine("Hello, {0}!", personName);
}
What would be the tests? There are at least four simple cases to test here:
The person name is null
. Is exception actually thrown? That's at least three lines of test code to write.
The person name is "Jeff"
. Do we get "Hello, Jeff!"
in response? That's four lines of test code.
The person name is an empty string. What output do we expect? What is the actual output? Side question: does it match the functional requirements? That means another four lines of code for the unit test.
The person name is short enough for a string, but too long to be combined with "Hello, "
and the exclamation point. What happens?¹
This requires a lot of testing code. Moreover, the most elementary pieces of code often require setup code which initializes the objects needed for the code under test, which also often leads to writing stubs and mocks, etc.
If the ratio is very big, in which case you may check a few things:
Is there code duplication across the tests? The fact that it's test code doesn't mean that the code should be duplicated (copy-pasted) between similar tests: such duplication will make the maintenance of those tests difficult.
Are there redundant tests? As a rule of thumb, if you remove a unit test, the branch coverage should decrease. If it doesn't, it may indicate that the test is not needed, since the paths are already covered by other tests.
Are you testing only the code you should test? You are not expected to test the underlying framework of the third-party libraries, but exclusively the code of the project itself.
With smoke tests, system and integration tests, functional and acceptance tests and stress and load tests, you add even more test code, so having four or five LOC of tests for every LOC of actual code is not something you should be worried about.
A note about TDD
If you're concerned about the time it takes to test your code, it might be that you are doing it wrong, that is code first, tests later. In this case, TDD may help by encouraging you to work in iterations of 15-45 seconds, switching between code and tests. According to the proponents of TDD, it speeds up the development process by reducing both the number of tests you need to do and, more importantly, the quantity of business code to write and especially rewrite for testing.
¹ Let n be the maximum length of a string. We can call SayHello
and pass by reference a string of length n - 1 which should work just fine. Now, at Console.WriteLine
step, the formatting should end up with a string of length n + 8, which will result in an exception. Possibly, due to the memory limits, even a string containing n / 2 characters will lead to an exception. The question one should ask is whether this fourth test is a unit test (it looks like one, but may have a much higher impact in terms of resources compared to average unit tests) and if it tests the actual code or the underlying framework.