Skip to content

Commit d2d1b27

Browse files
authored
Create automated_testing.md
1 parent 0d7ac3f commit d2d1b27

File tree

1 file changed

+156
-0
lines changed

1 file changed

+156
-0
lines changed

technical/automated_testing.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
---
2+
layout: default
3+
---
4+
5+
# Automated Testing
6+
7+
FreeCAD uses two different automated testing mechanisms, depending on the language being tested. The oldest, and most well-used test framework is the Python `unittest` system, which hooks directly into the FreeCAD Test Workbench. The second is the standalone Google Test C++ testing framework, which generates individual executables fo each part of the test suite.
8+
9+
## References
10+
11+
Some good references about automated testing:
12+
* [Back to Basics: C++ Testing - Amir Kirsh - CppCon 2022 - YouTube.](https://www.youtube.com/watch?v=SAM4rWaIvUQ)
13+
* [Practical Advice for Maintaining and Migrating Working Code - Brian Ruth - CppCon 2021 - YouTube.](https://www.youtube.com/watch?v=CktRuMALe2A)
14+
* [The Science of Unit Tests - Dave Steffen - CppCon 2020 - YouTube.](https://www.youtube.com/watch?v=FjwayiHNI1w)
15+
* [Michael Feathers. Working Effectively with Legacy Code. ISBN 0131177052.](https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052)
16+
* [Jeff Langr. Modern C++ Programming with Test-Driven Development: Code Better, Sleep Better. ISBN 1937785483.](https://www.amazon.com/Modern-Programming-Test-Driven-Development-Better/dp/1937785483/)
17+
18+
## Python Testing
19+
20+
Most Python workbenches in FreeCAD already have at least a rudimentary test suite, so no additional cMake setup should be required beyond simply adding your test file(s) to the cMakeLists.txt file. In addition, in Python it is very easy
21+
to create Mock functions and objects to reduce the dependency on external code, and/or to ensure you are testing only the isolated bit of code that you mean to. A typical Python unit test file might look like this:
22+
```python
23+
# SPDX-License-Identifier: LGPL-2.1-or-later
24+
25+
import unittest
26+
import unittest.mock
27+
28+
# Optional, allows your IDE to locate the appropriate test files to run outside FreeCAD if the code doesn't
29+
# depend on a FreeCAD import
30+
sys.path.append("../../")
31+
32+
# Here "Version" is the name of the class being tested
33+
class TestVersion(unittest.TestCase):
34+
35+
MODULE = "test_metadata" # file name without extension
36+
37+
def setUp(self) -> None:
38+
pass # Or do any setup you want to run before every test, creating objects, etc.
39+
40+
def tearDown(self) -> None:
41+
pass # Or to any cleanup work you need
42+
43+
def test_from_file(self) -> None:
44+
"""When loading from a file, the from_bytes function is called with the expected data"""
45+
from addonmanager_metadata import MetadataReader
46+
47+
MetadataReader.from_bytes = Mock()
48+
with tempfile.NamedTemporaryFile(delete=False) as temp:
49+
temp.write(b"Some data")
50+
temp.close()
51+
MetadataReader.from_file(temp.name)
52+
self.assertTrue(MetadataReader.from_bytes.called)
53+
MetadataReader.from_bytes.assert_called_once_with(b"Some data")
54+
os.unlink(temp.name)
55+
```
56+
57+
## C++ Testing
58+
59+
In an ideal world, a C++ unit test would be perfectly isolated from any external dependencies, which would be replaced with minimal, instrumented "mock" versions of themselves. However,
60+
this almost always requires that the code under test has been *designed* for testing, which is usually not the case for our existing code. In many cases you must add tests for the existing
61+
functionality and implementation, with all its deficiencies, before you can begin to refactor the code to make the tests better. There are many strategies for doing those "dependency injections",
62+
and over time we aspire to refactor FreeCAD such that it is possible, but developers are also encouraged to remember that:
63+
* "A journey of a thousand miles begins with a single step"
64+
* "How do you eat an elephant? One bite at a time."
65+
* "Perfect is the enemy of good"
66+
67+
A single not-perfect test is better than no test at all (in nearly 100% of cases). As a general rule, a single test should verify a single piece of functionality
68+
of the code (though sometimes that "functionality" is encompassed by multiple functions. For example, you will typically test getters and setters in pairs). Because your test functions will not
69+
themselves be "under test" it is critical that they be as short, simple, and self-explanatory as possible. A common idiom to use is "Arrange-Act-Assert", which in our test
70+
framework looks like this:
71+
```c++
72+
TEST(MappedName, toConstString)
73+
{
74+
// Arrange
75+
Data::MappedName mappedName(Data::MappedName("TEST"), "POSTFIXTEST");
76+
int size {0};
77+
78+
// Act
79+
const char* temp = mappedName.toConstString(0, size);
80+
81+
// Assert
82+
EXPECT_EQ(QByteArray(temp, size), QByteArray("TEST"));
83+
EXPECT_EQ(size, 4);
84+
}
85+
```
86+
87+
While you can write a series of standalone tests, it is often more convenient to group them together into a "test fixture." This is a class that your test is derived from, which can
88+
be used both to do setup and teardown, as well as to easily run all tests in the fixture without running the entire suite. Most IDEs recognize Google Test code and will offer the ability
89+
to run both individual tests as well as entire fixtures very easily from the IDE's interface. An example test fixture and associated tests:
90+
```c++
91+
// SPDX-License-Identifier: LGPL-2.1-or-later
92+
93+
#include "gtest/gtest.h"
94+
95+
#include "App/IndexedName.h"
96+
#include "App/MappedElement.h" // This is the class under test
97+
98+
// This class is the "Test Fixture" -- each test below is subclassed from this class
99+
class MappedElementTest: public ::testing::Test
100+
{
101+
protected:
102+
// void SetUp() override {}
103+
104+
// void TearDown() override {}
105+
106+
static Data::MappedElement givenMappedElement(const char* index, const char* name)
107+
{
108+
Data::IndexedName indexedName {index};
109+
Data::MappedName mappedName {name};
110+
return {indexedName, mappedName};
111+
}
112+
};
113+
114+
// Use the TEST_F macro to set up your test's subclass, derived from MappedElementTest
115+
TEST_F(MappedElementTest, constructFromNameAndIndex)
116+
{
117+
// Arrange
118+
Data::IndexedName indexedName {"EDGE1"};
119+
Data::MappedName mappedName {"OTHER_NAME"};
120+
121+
// Act
122+
Data::MappedElement mappedElement {indexedName, mappedName};
123+
124+
// Assert
125+
EXPECT_EQ(mappedElement.index, indexedName);
126+
EXPECT_EQ(mappedElement.name, mappedName);
127+
}
128+
129+
TEST_F(MappedElementTest, moveConstructor)
130+
{
131+
// Arrange
132+
auto originalMappedElement = givenMappedElement("EDGE1", "OTHER_NAME");
133+
auto originalName = originalMappedElement.name;
134+
auto originalIndex = originalMappedElement.index;
135+
136+
// Act
137+
Data::MappedElement newMappedElement {std::move(originalMappedElement)};
138+
139+
// Assert
140+
EXPECT_EQ(originalName, newMappedElement.name);
141+
EXPECT_EQ(originalIndex, newMappedElement.index);
142+
}
143+
```
144+
To run the tests, either directly run the executables that are generated (they are placed in the `$BUILD_DIR/test` subdirectory), or use your IDE's test discovery functionality to run just the tests for
145+
the code you are working on. FreeCAD's Continuous Integration (CI) suite will always run the full test suite, but it is advisable that before submitting a PR you run all tests on your local
146+
machine first.
147+
148+
The test directory structure exactly matches that of FreeCAD as a whole. To prevent ever having to link the entirety of FreeCAD and all of its tests into a single executable (at some point in the future
149+
when we have better test coverage!), the breakdown of the test executables mimics that of FreeCAD itself, with individual workbenches being compiled into their own test runners. To add a test runner to
150+
a workbench that does not have one, a developer should add a new target for their WB to the end of the cMakeLists.txt file at the top of the `tests` directory structure, e.g.
151+
```cmake
152+
add_executable(Sketcher_tests_run)
153+
add_subdirectory(src/Mod/Sketcher)
154+
target_include_directories(Sketcher_tests_run PUBLIC ${EIGEN3_INCLUDE_DIR})
155+
target_link_libraries(Sketcher_tests_run gtest_main ${Google_Tests_LIBS} Sketcher)
156+
```

0 commit comments

Comments
 (0)