Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you introduce unit testing into a large, legacy (C/C++) codebase?

People also ask

How do you introduce unit testing?

To perform unit testing, you have to test every output of your code unit with their expected return value to confirm if they adhere to their contract. Run your tests to confirm if your code unit is working as expected. Now, say you were to change the expected output of your test to a wrong value.

What is legacy code in testing?

Another commonly used definition of legacy code is code written without proper tests. The first definition (“ugly code”) does not necessarily rule out the second one (“code without tests”). We can say that there are many different cases when code can be called “legacy”.

What can be used for unit testing in C?

The most scalable way to write unit tests in C is using a unit testing framework, such as: CppUTest. Unity. Google Test.


we have not found anything to ease the transition from "hairball of code with no unit tests" to "unit-testable code".

How sad -- no miraculous solution -- just a lot of hard work correcting years of accumulated technical debt.

There is no easy transition. You have a large, complex, serious problem.

You can only solve it in tiny steps. Each tiny step involves the following.

  1. Pick a discrete piece of code that's absolutely essential. (Don't nibble around the edges at junk.) Pick a component that's important and -- somehow -- can be carved out of the rest. While a single function is ideal, it might be a tangled cluster of functions or maybe a whole file of functions. It's okay to start with something less than perfect for your testable components.

  2. Figure out what it's supposed to do. Figure out what it's interface is supposed to be. To do this, you may have to do some initial refactoring to make your target piece actually discrete.

  3. Write an "overall" integration test that -- for now -- tests your discrete piece of code more-or-less as it was found. Get this to pass before you try and change anything significant.

  4. Refactor the code into tidy, testable units that make better sense than your current hairball. You're going to have to maintain some backward compatibility (for now) with your overall integration test.

  5. Write unit tests for the new units.

  6. Once it all passes, decommission the old API and fix what will be broken by the change. If necessary, rework the original integration test; it tests the old API, you want to test the new API.

Iterate.


Michael Feathers wrote the bible on this, Working Effectively with Legacy Code


My little experience with legacy code and introducing testing would be to create the "Characterization tests". You start creating tests with known input and then get the output. These tests are useful for methods/classes that you don't know what they really do, but you know that they are working.

However, there are sometimes when it's nearly impossible to create unit tests (even characterization tests). On that case I attack the problem through acceptance tests (Fitnesse in this case).

You create the whole bunch of classes needed to test one feature and check it on fitnesse. It's similar to "characterization tests" but it's one level higher.


As George said Working Effectively with Legacy Code is the bible for this kind of thing.

However the only way others in your team will buy in is if they see the benefit to them personally of keeping the tests working.

To achieve this you require a test framework with which is as easy as possible to use. Plan for other developers you take your tests as examples to write their own. If they do not have unit testing experience, don't expect them to spend time learning a framework, they will probably see writing unit testings as slowing their development so not knowing the framework is an excuse to skip the tests.

Spend some time on continuous integration using cruise control, luntbuild, cdash etc. If your code is automatically compiled every night and tests run then developers will start to see the benefits if unit tests catch bugs before qa.

One thing to encourage is shared code ownership. If a developer changes their code and breaks someone else's test they should not expect that person to fix their test, they should investigate why the test is not working and fix it themselves. In my experience this is one of the hardest things to achieve.

Most developers write some form of unit test, some times a small piece of throw-away code they don't check in or integrate the build. Make integrating these into the build easy and developers will start to buy in.

My approach is to add tests for new and as code is modified, sometimes you cannot add as many or as detailed tests as you would like without decoupling too much existing code, err on the side of the practical.

The only place i insist on unit tests is on platform specific code. Where #ifdefs are replaces with platform specific higher level functions/classes, these must be tested on all platforms with the same tests. This saves loads of time adding new platforms.

We use boost::test to structure our test, the simple self registering functions make writing tests easy.

These are wrapped in CTest (part of CMake) this runs a group of unit tests executables at once and generates a simple report.

Our nightly build is automated with ant and luntbuild (ant glues c++, .net and java builds)

Soon I hope to add automated deployment and functional tests to the build.