Understanding legacy C code is a difficult task because
it is linked together tightly. New feature addition to this legacy code is even
more tedious than writing the feature independently from scratch. Majority of
the teams in VMware deals with some form of legacy code. There is often a
dilemma on how to validate and enhance this legacy code. One approach to
solving this is refactoring the code with unit tests. In this paper, we
describe a 3-layered approach to the Unit test framework where in each layer we
are addressing a unique problem.
The bottom layer, is where we design a set of generic
steps to build and compile your legacy C code with Google test framework
binaries and mock out all the dependent modules which might get invoked while
running these unit tests. The middle layer, is where we have extended the
framework to explain how we can run these Unit tests as part of continuous
integration and deployment (CI/CD) pipeline. Product components are assembled
by Jenkins CI/CD pipeline where they are built, unit tested, run through some
lightweight functional verification tests. This is integral for continuous
delivery in cloud development model. The top layer, is where we also generate
code coverage results as part of these Unit tests. This helps identify unused
or stale legacy code in the product for cleanup and areas lacking any coverage
can be addressed by writing more unit tests.
With VMware as a company shifting towards cloud
delivery model service, unit tests are essential for faster delivery and
quickly narrowing down issues. This paper not just helps in making writing Unit
tests simpler but also helps fill up functional test gaps of a legacy code.
The IEEE definition of unit
testing is the testing of individual hardware or software units or groups of
related unit 1,xiii. Writing unit tests for a legacy code is tedious and
labor intensive. While we have strong evidences, which proves that unit testing
can be leveraged to improve product quality, it is still not widely used as an
integral part of programming specially when it comes to legacy code which
mostly isn’t written with unit test in mind. It is a classic example of chicken
and egg dilemma: To write unit tests I need to refactor code and to refactor
code safely I need unit tests.
In this paper, we present an approach
to writing unit tests for Plain Old C code. The real difficulty in testing C
code is breaking the dependencies on external modules so you can isolate code
in units 2,ix. This can be especially
problematic when you are trying to get tests around legacy code. We have tons
of unit test frameworks available in the market for various languages but
integration of any one of them for Legacy C code was not directly applicable.
The rest of the paper is organized as follows: First, we
explain the Motivation behind the Idea to stress on the challenges we faced.
Second, we provide the details of our current solution and the work we have
done so far. Last but not the least we lay out the Future work that we have
planned for FoCUT.
2.1 Current Framework Limitations
Even though multiple unit test
frameworks already exist for C code, they lack the ability to isolate the code
by removing internal or external dependencies to make it unit testable. Some
which do provide isolation are mostly tedious and time-consuming and are often
complicated to implement.
C codes are linked together
tightly and it is harder to break dependencies during run time. Think of
virtual functions in Object Oriented Programming which serves this purpose.
Because of this tight coupling of C, there are basically two tools available
which are being leveraged for isolating the function under test – the C
preprocessor and the linker.
In the first approach, we can
eliminate functions either by using #define or by surrounding them using
compiler directives such as #if TEST … #endif to prevent them being compiled
into the released code. But this
requires modification in the production code which is not always desirable for
stable legacy codes. Using the linker substitution method, the code can easily
be replaced by mocks or stubs during linking time, the only problem being
rewriting of extra wrapper function for each mock or stub.
2.2 Test Driven Development
Test Driven Development (TDD) is a software development
practice which relies on the repetition of small development cycles. These
include writing of automated unit tests, which initially fail, for new feature
addition or improvement and then writing the minimum amount of code to pass
those tests and finally refactoring the code. TDD requires more code to be
written due to the inclusion of more number of unit tests which might seem to
be time consuming.
TDD results into more modularized and flexible code. It
requires the code to be treated and written as independent isolated units, tested
independently and then integrated together. These large number of independent
tests help in limiting the number of defects in the code. This isolation can
only be achieved efficiently by use of mock objects and stubs, which also helps
in the modularization of code. Present C unit test frameworks are not all
compliant with this TDD model, which makes TDD much more complex and