Objectives | Increase code quality by example |
Teacher | Luca Tricerri |
Duration | 3 sessions each of 1 hour |
Prerequisites | basic C++11 or Java |
Class Max Size | 10 |
Increase code quality using unit test via gtest and gmock.
- Unit test introduction
- How to unit test with gtest
- What is moking?
- How to mock with gmock
End of January, if possible in presence.
- Github account (https://github.com/)
- Gitpod account (https://www.gitpod.io/)
- Fork this repository
- To open the forked repo on Gitpod use the following button. It will take a while the first time.
Optional
- Installed Gitpod Extension for Chrome (https://chrome.google.com/webstore/detail/gitpod-always-ready-to-co/dodmmooeoklaejobgleioelladacbeki)
- Authorized Access
❓ So how can I test my software? There are several ways to do that
.
UNIT TESTING is a type of software testing where individual units or components of the software are tested. The purpose is to validate that each unit of the software code performs as expected. Unit Testing is done during the development (coding phase) of an application by the developers. Unit Tests isolate a section of code and verify its correctness. A unit may be an individual function, method, procedure, module, or object.
The so called tests pyramid
❓ What are the advantages to write unit tests?
Unit tests help to fix bugs early in the development cycle and save costs. In this stage the software is easier to debug:
- It is smaller.
- It is less interdependent to others parts.
📌 Marconi's approach to software testing.
Marconi was a large company that developed a particular mobile phone technology called TETRA. It was a kind of GSM / UMTS / 4G mobile phone network for police and military forces.
They had created a test group in Florence with about 20 telecommunications engineers, they had poor results.
The head of the test group decided to take 20 philosophy graduates instead of engineers. I was among the ones that developed the TETRA, I was there in that mess. In the beginning, the so-called monkey tests were an incredible success. Bug after bugs, but... as the testers were not technicians they report on the tests were almost impossible to reproduce. So in the end, the 20 philosophy graduates went back to their studies.
The system was too complex to be tested all together.
When you have a suite of unit tests, you can run it iteratively to ensure that everything keeps working correctly every time you add new functionality or introduce changes. This helps refactoring a lot.
❓ What is refactoring?
Using an Agile methodology we continuously develop new and unplanned features from the beginning. In this context, the application architecture may become unstable. Periodic refactoring is important.
Running, debugging, or even just reading tests can give a lot of information about how the original code works, so you can use them as implicit documentation.
Note that in this way the code documentation is always updated (otherwise the code does not compile)
Unit testing helps to improve code coverage.
❓ What is test coverage?
It is a technique to ensure that your tests are testing your code or how much of your code you exercised by running the test. Are there code parts not tested?
- It requires time, and I am always overscheduled My code is rock solid! I do not need unit tests.
- Programmers think that Integration Testing will catch all errors and do not execute the unit test. Once units are integrated, very simple errors which could have been very easily found and fixed in unit tested take a very long time to be traced and fixed.
The truth is Unit testing increase the speed of development.
- Creating tests for all publicly exposed functions, including class constructors and operators.
- Covering all code paths and checking both trivial and edge cases, including those with incorrect input data (negative testing).
- Assuring that each test works independently and does not prevent other tests from execution.
- Organizing tests in a way that the order in which you run them does not affect the results.
A single unit test is a method that checks some specific functionality and has clear pass/fail criteria. The generalized structure of a single test looks like this:
Test (TestGroupName, TestName) {
- setup block
- running the under-test functionality
- checking the results (assertions block) }
As code's testability depends on its design, unit tests facilitate breaking it into specialized easy-to-test pieces.
An easy way to do this is to use self-consistent classes.
Another useful technique is the so-called dependence injection.
It is a technique in which an object receives other objects. The receiving object is called a client and the passed-in (injected
) object is called a service.
With no partcular tecnique:
class MyClass
{
private:
Database mydatabase_;
public:
int getIntFromDatabase(const std::string& query)
{
...
mydatabase_.get(query);
...
}
};
With inheritance
class MyClass: public Database
{
public:
int getIntFromDatabase(const std::string& query)
{
...
get(query);
...
}
};
With dependency injection
class MyClass
{
private:
Database& mydatabase_;
public:
MyClass(Database &mydatabase):mydatabase_(mydatabase)
{}
int getIntFromDatabase(const std::string& query)
{
...
mydatabase_.get(query);
...
}
};
The use of the inheritance technique tightly couples parent class with child class. It is harder to reuse the code and write units.
- https://www.jetbrains.com/help/clion/unit-testing-tutorial.html#basics
- https://www.guru99.com/unit-testing-guide.html
- Test coverage: https://www.guru99.com/test-coverage-in-software-testing.html#1
- https://betterprogramming.pub/13-tips-for-writing-useful-unit-tests-ca20706b5368
googletest is a testing framework developed by the Testing Technology team with Google’s specific requirements and constraints in mind. Whether you work on Linux, Windows, or a Mac.
- very well done
- very well supported
- cmake friend
- visual studio code friend
- cross-platform
TEST(Multiplier, check_multiply_001)
{
Multiplier mult;
EXPECT_EQ(8/*expected*/, mult.invoke(4, 2)/*current*/);
}
CODE: See test:testMultiplier.cpp
This is only a note to remember to correctly order the expected and current value.
EXPECT_EQ(8/*expected*/, mult.invoke(4, 2)/*current*/);
The unit test framework usually gives the error log based on this assumption.
./training-programming-best-practices/unittest-course-part/src/unittest/testMultiplier.cpp:21: Failure
Expected equality of these values:
8
mult.invoke(4, 2)
Which is: 7
[ FAILED ] Multiplier.Test_simple001 (0 ms)
Choose good names for tests, see for reference:https://dev.to/canro91/unit-testing-best-practices-6-tips-for-better-names-524m.
- Choose a naming convention for your test names and stick to it.
- Every test name should tell the scenario under test and the expected result. Do not worry about long test names. But, do not name your tests: Test1, Test2, and so on.
- Describe in your test names what you are testing, in a language easy to understand even for non-programmers.
❌ Do not prefix your test names with "Test". If you're using a testing framework that does not need keywords in your test names, don't do that.
📌
By Joel:
the infamous Hungarian naming convention:
https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/
Don't mix test code with production code (never):
class WinterAssistant {
public:
bool isTest_ = false;
bool needWinterJacket() {
Thermometer *thermometer;
if( isTest_ )
{
thermometer = new FakeThermometer();
} else
{
thermometer = new Thermometer();
}
return thermometer->getTemperature() < 40;
}
}
In this case, you can use easily the code injection technique.
Just as you should be running your tests as you develop, they should also be an integral part of your continuous integration process. A failed test should mean that your build is broken.
Other EXPECT and ASSERT macro exist:
EXPECT_TRUE
EXPECT_FALSE
EXPECT_EQ
EXPECT_NE
EXPECT_GT
...
ASSERT_TRUE
ASSERT_FALSE
ASSERT_EQ
ASSERT_NE
ASSERT_GT
...
FAIL
See also:
https://github.com/google/googletest/blob/main/docs/reference/assertions.md
ASSERT vs EXPECT
EXPECT_EQ(8/*expected*/, mult.invoke(4, 2)/*current*/
ASSERT_EQ(8/*expected*/, mult.invoke(4, 2)/*current*/
Assert abort the current test while Expect gives only the error but goes on. Expect is preferred.
If you find yourself writing two or more tests that operate on similar data, you can use a test fixture. This allows you to reuse the same configuration of objects for several different tests.
CODE: See test:testMultiplierParamAndFixture.cpp
CODE: See test:testMultiplierParamAndFixture.cpp
For testing private members we can use one of the c++ most hidden
features.
It is possible to change visibility over inherited members.
class Multiplier
{
protected:
virtual void internalInvoke();//To be tested
};
class TestMultiplier : public Multiplier
{
public:
using Multiplier::internalInvoke;
};
The only prerequisite is that the method should be virtual
.
CODE: See test:testMultiplierInternal.cpp
Also thrown exceptions can be tested.
EXPECT_THROW(mult.invoke(10, 2), std::invalid_argument);
CODE: See test:testMultiplier.cpp
If we expect that a function closes the program with an error that contains "must be true".
TEST(MyDeathTest, Exit)
{
MyClass myclass();
EXPECT_DEATH( { myclass.foo(); }, "must be true");
}
Plain exits:
TEST(MyDeathTest, NormalExit)
{
EXPECT_EXIT(NormalExit(), testing::ExitedWithCode(0), "Success");
}
Exit due to a signal and an error that match "Sending myself unblockable signal"
TEST(MyDeathTest, KillProcess)
{
EXPECT_EXIT(KillProcess(), testing::KilledBySignal(SIGKILL),"Sending myself unblockable signal");
}
Quite easy to write:
#include "gtest/gtest.h"
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
For filtering the tests add:
::testing::GTEST_FLAG(filter) = "Test.Test_003";
or you can use wildcard:
::testing::GTEST_FLAG(filter) = "Test.T*";
VC is perfectly integrated with gtest, install the test explorer extension see:
https://marketplace.visualstudio.com/items?itemName=matepek.vscode-catch2-test-adapter
The test explorer:
✔️ It could be necessary to add in settings.json:
testMate.cpp.test.executables": "${workspaceFolder}/install/bin/unittest"
With your install path.
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip
)
https://google.github.io/googletest/primer.html
Change this code using the code injection technique.
class CanSenderScheduler {
public:
#ifndef TEST
bool isTest_ = false;
#else
bool isTest_ = true;
#endif
bool sendData(const std::vector<unsigned char> &data)
{
CanServer *server;
if( isTest_ )
{
server = new FakeCanServer(12);
} else
{
server = new CanServer(12);
}
return server->send(data);
}
};
class CanServer
{
public:
CanServer(unsigned int address);
bool send(const std::vector<unsigned char> &data);
}
- Complete the division class im
divisionlib
(take a look at the Multiplier class) - As input, the method invokes, can have only numbers > -30.
- Add the unit test for the class. Be careful with the edge cases.
The skeleton is in testDivision.cpp
To test your complex class without the connected/related classes, you could use a mocking framework. Not always is possible to obtain the desired result, it depends mainly on how your code is written. If you are using dependency injection, the testability of the code is much better since you can inject mocks as well.
A mock object implements the same interface as a real object (so it can be used as one), but lets you specify at run time how it will be used and what it should do (which methods will be called? in which order? how many times? with what arguments? what will they return? etc...).
Mocking is a way to replace a dependency in a unit under test with a stand-in for that dependency. The stand-in allows the unit under test to be tested without invoking the real dependency.
Stub: Stub is an object that holds predefined data and uses it to answer calls during tests. Such as an object that needs to grab some data from the database to respond to a method call.
Mocks: Mocks are objects that register calls they receive. In test assertion, we can verify on Mocks that all expected actions were performed. A mock is like a stub but, the test will also verify that the object under test calls the mock as expected. Part of the test is verifying that the mock was used correctly.
Also, the mocks can be programmed
to give certain result values or behaviours.
We are using gmock which is the moking library for gtest.
When using gMock,
- first, you use some simple macros to describe the interface you want to mock, and they will expand to the implementation of your mock class.
- next, you create some mock objects and specify their expectations and behaviour using an intuitive syntax.
- then you exercise code that uses the mock objects. gMock will catch any violation of the expectations as soon as it arises.
Mocking needs dependency injection to work better. See above
This part of the course will be done by examples.
First of all, you need to mock the class and method we do not want to test directly.
class ...
{
...
MOCK_METHOD(double, getDataFromFile, (unsigned int), (const, override)); // note the parentesis
}
CODE: See test:testMultiplierFromFile.cpp
CODE: See test:testMultiplierFromFile.cpp
In this case, is necessary to write a class to overlap the c API.
See file: InterfaceForCApi.h
CODE: See test:testMediaScanner.cpp
http://google.github.io/googletest/gmock_cook_book.html
Mock and write unit tests for pwmlib
. You can find the skeleton in testPwm.cpp
.
How to:
-
Fork the training repo
-
Only for Visual Studio Code users. Install extension on Visual Studio Code. This step is not mandatory.
-
Enter GitPod with the button (the first time it will take 5minutes).
-
Create build folder, create makefile and compile
cd /workspace/training-programming-best-practices/unittest-course-part mkdir build cmake -DCMAKE_INSTALL_PREFIX=/workspace/training-programming-best-practices/unittest-course-part/install .. make install
-
Execute UT
cd /workspace/training-programming-best-practices/install/bin
./unittest