What is link-time substitution used for?
Link-time substitution is one of three methods to create test doubles for your project. Test doubles are used to replace pieces of code that your software depends on (the code under test, or CUT), but that are hard to use in combination with (automated) tests like unit tests.
For embedded software developers the most recognizable difficult to test code will probably be code that is hardware dependent. Without test doubles, it is not possible to unit test how your CUT responds to, for example, a temperature sensor that outputs different temperatures, or a sensor that breaks while the application is running.
There are two other big reasons to want test doubles for a certain piece of code. If the CUT makes use of a library function that takes a long time to execute, the total time it takes to unit test can increase drastically if you write a lot of tests. A test double can be used to return a mock value instantly, without having the long wait. Volatile code also is difficult to use in your tests. A good example of this is if the code you want to tests makes use of a clock. A test double can be used to make your code think it is, for example, 8 A.M., and then the test can be used to verify whether the behavior of the CUT is correct (for example, an alarm should go off at 8 A.M.). But you could also use test doubles to simulate network failures for example.
How does link-time substitution work?
Link-time substitution replaces the untestable code your CUT depends on, and replaces it with code that is suitable for testing. Lets say we are writing code for a digital thermometer. The software uses a function called readAdc() to get the output of the internal temperature sensor of the thermometer. ReadAdc() is provided by the operating system your application is running on.
If we were to write unit tests for this function, we would like to verify if certain outputs from readAdc() get converted to a correct temperature reading. Of course, we would also like to check how our function deals with readAdc() when it returns unexpected results, such as an error code because the internal temperature sensor is broken. These are things we cannot do without test doubles, because it is impossible for us to manipulate the output of readAdc() (without tampering the device physically). So we want to make a test double for readAdc() that we can manipulate in our unit tests.
Now, as a simple start, we could implement readAdc() in FakeAdc.c to always return 3000. This way, we can test in our unit test if readSensor() performs the correct behaviour, knowing that when it calls readAdc() it will get a return value of 3000. Meanwhile, our regular production code still uses the implementation in adc.c, and for that code nothing changes. The images below show how this works.
FakeAdc.obj will be used by the testing code
adc.obj will be used by the production code
With the current implementation of FakeAdc.c we are only able to test what happens when the ADC reads a value of 3000. Obviously, this isn’t super useful for testing yet. To give FakeAdc.c some actual value for our tests, we give it some additional functionality that adc.c does not have. We start by creating a new header for our adc test double, called FakeAdc.h. In this file, we define some of the extra functionalities we would like to have, and we also include the original Adc.h. This way, if anything changes in Adc.h but we do not also change this in our FakeAdc.c file, the compiler will warn us.
Now when we want to unit test what happens when readAdc() returns an error code instead of a measurement, we can call setAdcBroken(true) in our unit test before calling readSensor(). We can also call setAdcVal() to test what readSensor() returns, based on varying return values of readAdc(). And all this can be done without changing a single line in the production code.
So, now you have a basic overview of how link-time substitution works. You should be able to use this in almost any C/C++ project. Below is a very simple example from one of my tests, where I used test doubles to be able to manipulate what the value of a digital input should be.
The biggest drawback of link-time substitution however, is that you are only able to replace entire files. If you want to mock only certain functions from a file, while keeping the original implementations of the other functions link-time substitution is not the way to go. You should probably look at function pointer if you want to achieve this. A big advantage of link-time substitution over function pointer substitution is that you will not have to restructure your code for link-time substitution, while the same cannot be said for function pointer substitution.
Source: Medium - Bram Tertoolen
The Tech Platform