Skip to content

Unit Testing

Franz Miltz edited this page Sep 15, 2021 · 2 revisions

What is a Unit Test?

A unit test is the smallest possible test for a piece of software; each test only tests one function or method. For example, the state machine may test only one transition based on one condition; or navigation would test the IMU set up and execution in different tests.

Creating a Unit Testing File

  1. Go to the directory hyped-2021/test/src/<ModuleYouWishToTest>
  2. Create the file <NameOfTest>.test.cpp
  3. At the top of this file you must include the google test library header
  4. Now go to Test.files which should be in hyped-2021/test/src/ and add the name of the file in step 2 with its container under the sources followed by a back slash

For Example

If I wished to create a test file for the config in utils then each step would look like:

  1. hyped-2021/test/src/utils
  2. config.test.cpp
  3. At the top of config.test.cpp I have #include "gtest/gtest.h"(and for mock #include "gmock/gmock.h")
  4. Under SRCS := \ I have utils/config.test.cpp \

Running and Excluding Tests

Unit tests should run automatically on commit. To run them manually, run make test from root directory. To exclude tests from running, i.e. labeling a test as a production test , append _prod to the end of the test name. e.g. TEST(suite_name, testname_prod) { ... }. (works with TEST_F too)

GTEST_FILTERS should be set to two :-separated lists of test names (including suite name), the lists themselves separated with a -. A test will run if it matches at least one of the names in the first list, and matches none of the names in the latter. A * can be used as a wildcard to match any string.

e.g. make test-filter GTEST_FILTERS=*_endswiththis:*contains_this*:InThisSuite.*-*_endswiththat:*contains_that*:InThatSuite.* runs every test which either: ends with '_endswiththis', contains 'contains_this', or starts with 'InThisSuite.' (which means it is in the test suite, InThisSuite) as long as the test doesn't end with '_endswiththat', and doesn't contain 'contains_that' and doesn't start with 'InThatSuite'.

For more information: Google Test Github Page

Recommend to search (ctrl + f or equivalent) for 'gtest_filter'

Unit Testing Fundementals

Assertions

We use assertions in our tests to check if values are as we expect them to be. If an assertion fails then our test fails.

A basic assertion may check a binay condition; ASSERT_TRUE(_data.getBrakesDeployed() == true) or may test an equality ASSERT_EQ(squareNumber(2), 4).

For each unit test aim to have only one assertion. This is so that each test is only testing one thing at a time, making it easier to hone in on what exactly is failing.

GTest provides a number of assertions, for more detail check here.

Unit Test Anatomy

We recommend using test fixtures when constructing your test suite. A test fixture is a set of common test; a test fixture lets you perform a set up and tear down before each test is run, saving on repeated code.

If you require to get a data instance in each test, you can add this to a test fixture and it will be run before every test. If you were writing a test the function setStateMachine, before each test we would require an instance of the state machine. Rather than running the set up in each function, we add it to SetUp() which is run before each test.

We can also perform actions after each test using TearDown; this may include releasing memory or resetting modules.

Template

struct <TestFixtureName> : public ::testing::Test {
  protected:
    // Add here any types you wish to use across your classes
    <ObjectType> <object>
    void SetUp()
    {
      // Here define the properties of any new types
      <object>.<objectFunction>(<objectFunctionArgument>);
    }
    void TearDown() {}
};

// Tests which belong to a test fixture are labeled TEST_F
TEST_F(<TestFixtureName>, <UniqueUnitTestName>)
{
  // Any assertions or further setting up needed go here
  ASSERT_EQ(<argument1>, <argument2>);
}

Example

/*
 * Checks setStateMachineData correctly updates current
 * state in hyped::data::State
 */

  struct StateMachineTest : public ::testing::Test {
    protected:
      utils::Logger log_;
      data::StateMachine state_machine_;
      data::Data *data_;

      // Run before each test
      void SetUp() {
        data_ = &Data::getInstance();
        state_machine_ = _d->getStateMachineData();
      }
      void TearDown() {}
  };

  // Define tests
  // Test when we set our state to a valid state,
  // it reflects in the data instance
  TEST_F(StateMachineTest, SetToNormal) {
    state_machine_.current_state = data::State::kAccelerating;
    data_->setStateMachineData(_sm);
    ASSERT_EQ(_d->getStateMachineData().current_state,
              data::State::kAccelerating);
  }

  // Test when we set our state to an invalid state, it fails
  // the pod 
  TEST_F(StateMachineTest, SetToIncorrect) {
    state_machine_.current_state = data::State::InvalidState;
    data_->setStateMachineData(_sm);
    ASSERT_EQ(data_->getStateMachineData().current_state,
              data::State::CriticalFailure);
  }

Unit testing is great for small functions, however, it does not work very well on highly coupled code (where your function depends on lots of functions); as a failure may not be attributed to your code, it may result from a failure elsewhere. To solve this we use mocking.

Clone this wiki locally