|
| 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