The Hello World program is the best way to test a new language or concept. How can you apply this concept to a Unit Testing Framework, specifically to Google Test?
In total you have to Setup Google Test for your platform, write a simple function in C and write a basic test in C++ that calls the C function and asserts its return value. In order to use the C function in the C++ test code you have to use the keyword extern.
I will show you the complete source code for this in the paragraphs ahead. I will also go a step further and start programming a simple calculator (without UI) with Unit Tests by using the TDD method.
A Hello World Program in Google Test with Pure C
A Hello World program is usually the first program you write if you try out a programming language that is new for you. It shows you how to setup the code and any dependencies and prompt a simple message that states “Hello World”.
For Google Test (or any other Unit Testing Framework) the equivalent of this is a failing and a passing test together with a very simple function that represents the Code Under Test, also sometimes referred to as System Under Test (SUT).
As I already pointed out in another article, it is indeed possible to test pure C programs with the C++ Google Test Framework, but you have to write the test code itself in C++ and include your C code with the extern keyword. For this reason (and for several other reasons) it’s a good idea to put your test code into a different project.
The GTest Hello World in C Example
Our Hello World test program is designed to serve two purposes: First it should show a failing test and second it should show a passing test and both should fail respectively pass for the right reasons.
These are the header and the source file of our fictional C program. It contains only one function that returns a constant value.
#pragma once
const char* greetings();
#include "HelloWorld.h"
const char* greetings()
{
return "Hello World";
}
And here is our C++ test file. The first test FailingOnPurpose wants to assert that a certain value is true, but we give it a constant false. The reason to fail is therefore expected true but is false. The second test expects the message “Hello World” from the C function greetings() as a return value. It passes because we returned exactly that in the C code above.
#include <gtest/gtest.h>
extern "C" {
#include "HelloWorld.h"
}
TEST(HelloWorld, FailingOnPurpose) {
ASSERT_TRUE(false);
}
TEST(HelloWorld, Greetings) {
ASSERT_EQ("Hello World", greetings());
}
If you run the test you will get one passing and one failing test, making the overall result fail. The error message indicates that it fails for the right reason. The screenshot shows the result in the Microsoft Visual Studio 2019 Test Explorer:
Programming a Calculator with TDD and Unit Tests in Pure C
Now we get a bit more sophisticated. We will start programming a calculator by using the TDD method. Test Driven Development means that we first write a tests for one specific requirement, then only write the code to make the test pass and then refactor the code for quality and clarity purposes.
The key is to only refactor when all tests are passing and to refactor every time a requirement is implemented and passing. Then we move on to the next requirement. In addition, it may be that one requirement requires several tests.
The First Test
We will start with the requirement (or feature) of addition. The first step is writing a test that fails on purpose. With the first test we want to make sure that two plus two equals four.
Failing to compile counts as a Failing Test
#include <gtest/gtest.h>
extern "C" {
#include "Calculator.h"
}
TEST(Calculator, AddTwoNumbers) {
ASSERT_EQ(4, add(2, 2));
}
The code does not compile yet (which counts as a failing test), so we have to stop and write only enough code to make it compile:
#pragma once
int add(const int first, const int second);
#include "Calculator.h"
int add(const int first, const int second)
{
return 0;
}
Now we can run the test and see that it fails:
We make it pass by returning four as a constant. It may seem ugly but remember: We first make the test pass, then we refactor it for quality and clarity.
#include "Calculator.h"
int add(const int first, const int second)
{
return 4;
}
Now the test passes:
There is nothing to refactor yet, so we move on to the next test.
The Second Test
We have all tests passing but we know that the requirement is not yet fulfilled. We have two unused input variables and we only get the correct results for two plus two or one plus three because our function returns a constant value. We want to make it more generic with the help of Triangulation.
Describe Triangulation
I admit that Triangulation is an overhead for this purpose. If it were in the real world, I would replace the constant 4 with the correct code during the refactoring phase and move on. It all depends on you and how confident you are about the code. Also use common sense.
The second test expects the sum of five plus eight (two randomly chosen numbers) and sure fails after we run all the tests:
TEST(Calculator, AddAnotherTwoNumbers) {
ASSERT_EQ(13, add(5, 8));
}
The solution is of course simple. It also gets rid of the constant which moves the program from specific to generic which is always a goal in software development.
int add(const int first, const int second)
{
return (first + second);
}
We run all the tests to see the new test passing and also make sure that we did not break anything in the rest of the code:
Everything is fine and I think that we can consider the “Add two numbers” requirement as done.
Where To Go From Here
The next steps for the calculator could be to implement the other basic arithmetic functionality and creating a GUI so that the user can input his numbers and see the actual output. If you are interested in creating a GUI with pure C, you can read my article on the topic.