gBMCWeb Unit Testing Playbook

Objective

This document is for developers that wish to write a unit test for bmcweb.

This document should cover all the libraries that are used when writing a unit test for bmcweb.

Running Unit Tests

If you already know how to run unit tests locally, please skip this section.

Otherwise please read the “Docker native builds and unit tests” section in README_GOOGLE.md

Writing Unit Tests for GET Handlers

Please take a look at this example commit to follow along.

These are the following steps to write a gbmcweb unit test for GET.

Step #0: Create a new file for the unit test if it doesn't already exist

If I am testing a handler in redfish-core/lib/managers.hpp, and there is no test file, then please create one. We are creating a corresponding test file per redfish-core library file.

If you do create a file, please add the following file path to the test file in srcfiles_unittest list in the meson.build. This adds the new unit test file to the meson build and will automatically test it in all future presubmits.

Step #1: Ensure your handler has its own function

If the handler you are unit testing looks something like below, you can skip this step.

    BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/")
        .privileges(redfish::privileges::getChassis)
        .methods(boost::beast::http::verb::get)(
            std::bind_front(handleChassisGet, std::ref(app)));

Otherwise if your handler has an explicit lambda function defined instead of a reference to a method call like below, please refactor the code such that the handler has its own function. This is so that we can maximize the unit test coverage for this handler.

Here is an example commit that includes this refactoring. (Yes this is not a GET handler but this applies to all unit tests)

Step #2: Create a unit test fixture with SnapshotFixture

All gBMCWeb unit tests will be either using or inheriting from the test fixture defined in snapshot_fixture.hpp. This is because this class has the necessary setup required to deserialize managedStore.

Starting your test with “TEST_F(SnapshotFixture, $NAME_OF_TEST)” is enough.

Step #3: Call the handler in the unit test

Example call:

    unitTestHandlerFunction(app_, CreateRequest(), share_async_resp_);

The call above is the most basic handler call. Some unit tests may require extra parameters to be passed in, but those should be URL ids such as system or chassis name.

Note: app_and share_async_resp_ are class variables of SnapshotFixture

Note: CreateRequest() creates a crow::Request for the handler. For the purposes of GET requests, no parameters need to be passed in

Step #4: Run the IO Context

In order to catch dangling references passed to DBus callbacks in unit tests, we actually run the DBus call passed into the managed store in an io context instead of triggering the callback immediately.

This means the io context must be run in order for the response to be populated correctly. This can be done with a simple method call.

    // Perform dbus calls
    RunIoUntilDone();

Step #5: Test the async_resp object

We can now use EXPECT_EQ statements to test the response object. Sample expect statements are provided below:

// Test properties of share_async_resp_->res.jsonValue
    EXPECT_EQ(json["@odata.id"], "/redfish/v1/Chassis");
    EXPECT_EQ(json["@odata.type"], "#ChassisCollection.ChassisCollection");
    EXPECT_EQ(json["Name"], "Chassis Collection");

// Ensure response is OK
    EXPECT_EQ(share_async_resp_->res.result(), boost::beast::http::status::ok);

Missing DBus Objects?

If you are missing DBus objects, you have two options.

Option #1: Take a new Snapshot

This option is recommended if you are missing a lot of DBus objects from the existing snapshots. If it would be easier to just take an entirely new snapshot instead of mocking one or two DBus objects, then please follow the steps below on “How to take a new snapshot”

Usually this should be the preferred method if a handler is meant for a certain platform which there is no current existing snapshot of.

Option #2: DBus Mocking

This is directly creating the DBus object you want to test for and manually inserting it into the managedStore. Here is an example.

Take a look at this example commit and follow along.

We want to test the URI “/redfish/v1/Systems//LogServices/FaultLog/Entries”.

This makes one DBus call that looks like the following:

managedStore::GetManagedObjectStore()->getManagedObjectsWithContext(
        "xyz.openbmc_project.Dump.Manager", {"/xyz/openbmc_project/dump"}...);

You can see that this DBus call is made in the test logs. If you invoke this line in the unit test and check the logs at “build/meson-logs/testlog.txt” (I use build for my meson build artifacts).

You will see the following lines which reference what key was used to search managedStore for the DBus call. This specific DBus call has a log like this.

Object found in store.  Key: kManagedObject|xyz.openbmc_project.Dump.Manager|/xyz/openbmc_project/dump

This will happen if it finds an object in the store.

kManagedObject|xyz.openbmc_project.Dump.Manager|/xyz/openbmc_project/dump1 not found in cache.

If it does not, it should also print what key it tried.

Regardless, you should be able to find the full key used to search the snapshot and with this information you can either add or update the value using upsertMockObjectIntoManagedStore as seen in the commit above.

To create the actual value to be inserted into managedStore, we utilize CreateMockFaultLogEntry to mock the DBus object to be inserted.

NOTE: test/g3/mock_managed_store_test.cpp:258 has examples on creation of fake DBus objects.

With all these steps complete, the DBus call you were trying to mock should just now return what you upserted into the managedStore.

So in total, the steps will be:

  1. Find out the exact key you need to mock the value for and create a KeyType object with those exact parameters.
  2. Create the ValueType object of the result you want to corresponding DBus query to output
    1. Note: The ValueType object must be a shared_ptr as that is what managedStore stores.
    2. You can use managedStore::MockManagedStoreTest::CreateValueType() to help create the object.
  3. Utilize upsertMockObjectIntoManagedStore to update/insert the object in managedStore
  4. Run the unit test and you should see the object you mocked in as the response to the DBus call.

Note: Several other methods have been included to interface managedStore.

evictMockObjectFromManagedStore: This will remove a key value pair from managedStore

getMockObjectFromManagedStore: This will get the corresponding value from a KeyType in managedStore

printObjectFromManagedStore: This will log the corresponding value from the KeyType for debugging.

Writing Unit Tests for All Other Handlers

Please take a look at this example commit to follow along.

These are the following steps to write a gbmcweb unit test for GET/POST/PATCH/DELETE

Step #1: Ensure your handler has its own function

This is the exact same step 1 as above. Please refer to Step #1 in “Writing Unit Tests for GET Handlers”.

Step #2: Create a unit test fixture with SnapshotFixture

This is the exact same step 2 as above. Please refer to Step #2 in “Writing Unit Tests for GET Handlers”.

Step #3: Setup expectations for DBus calls made

We are going to create an EXPECT_CALL for each Dbus call made. To see how to set these up, we will take a look at the example commit above.

Specifically in handleDeleteEventLogEntry, the following DBus call is made

    managedStore::GetManagedObjectStore()->PostDbusCallToIoContextThreadSafe(
        strand, respHandler, "xyz.openbmc_project.Logging",
        "/xyz/openbmc_project/logging/entry/" + entryID,
        "xyz.openbmc_project.Object.Delete", "Delete");

Calls to PostDbusCallToIoContextThreadSafe must have an associating EXPECT_CALL in order to properly mock the behavior in the unit test.

The following EXPECT_CALL would work for the above file. (Assume entryId is “entry1”)

// Simulate a Failed DBus Call
    // Setup dbus mocking
    EXPECT_CALL(*managedStore::GetManagedObjectStore(),
                PostDbusCallToIoContextThreadSafe(An<absl::AnyInvocable<void(const boost::system::error_code&)>&&>(),
                "xyz.openbmc_project.Logging", "/xyz/openbmc_project/logging/entry/entry1",
                "xyz.openbmc_project.Object.Delete", "Delete"))
        .Times(1)
        .WillOnce(SimulateFailedAsyncPostDbusCallAction::
                    SimulateFailedAsyncPostDbusCall());
//Simulate a Succesful DBus Call
    EXPECT_CALL(*managedStore::GetManagedObjectStore(),
                PostDbusCallToIoContextThreadSafe(An<absl::AnyInvocable<void(const boost::system::error_code&)>&&>(),
                "xyz.openbmc_project.Logging", "/xyz/openbmc_project/logging/entry/entry1",
                "xyz.openbmc_project.Object.Delete", "Delete"))
        .Times(1)
        .WillOnce(SimulateSuccessfulAsyncPostDbusCallAction::
                    SimulateSuccessfulAsyncPostDbusCall());

Things to note:

  1. We are expecting the global managedStore object to perform the DBus call. This is marked by *managedStore::GetManagedObjectStore() as >the first parameter
  2. We must pass in all parameters expected in the DBus call in order to properly mock it.
    1. The callback must actually use An<absl::AnyInvocable<..>>
    2. An can be brought in using ::testing::An.
  3. .Times() shows the amount of times we expect this call to occur
  4. The defining action can be one of the following:
    1. SimulateFailedAsyncPostDbusCallAction::SimulateFailedAsyncPostDbusCall()
    2. Calls the callback with ec != 0. (An error occurred during Dbus call)
    3. SimulateSuccessfulAsyncPostDbusCallAction::SimulateSuccessfulAsyncPostDbusCall()
    4. Calls callback with ec ==0. (Successful DBus call)

Step #4: Call the handler in the unit test

This is similar to step 3 above with one big note.

Methods like POST and PATCH require json data that need to be sent. This can actually be done using CreateRequest. If I wanted to simulate the following curl request below

curl -X PATCH http://localhost/redfish/v1/Systems/system/LogServices/EventLog/Entries/entry1 -d '{"ResetType":"ForceOff"}'

This can be simulated by calling the handler in the following way:

    handlePatchEventLogEntry(app_, CreateRequest("{\"Resolved\": true }"), share_async_resp_, "system", "entry1");

The json string can be directly passed into CreateRequest to simulate the data being sent.

Step #5: Run the IO Context

In order to catch dangling references passed to DBus callbacks in unit tests, we actually run the DBus call passed into PostDbusCallToIoContextThreadSafe in an io context instead of triggering the callback immediately.

This means the io context must be run in order for the response to be populated correctly. This can be done with a simple method call.

    // Perform dbus calls
    RunIoUntilDone();

Step #6: Test the async_resp object

This is the same as above. We are just validating the response object.

How to take a new snapshot

There will be situations where the current snapshot is not enough and you need a new snapshot for your own special platform or configuration. The steps to perform this are very simple.

Process 1: If you are a Googler

Please follow the “How to take a new snapshot” section at go/gbmcweb-unit-testing-playbook.

Process 2: If you are not a Googler

Please be ABSOLUTELY sure that you are not uploading sensitive information to this public repo. If you need help please reach out to edwarddl@ to see if your snapshot can be uploaded or not.

Step 1: Get a machine with bmcweb that you would like to mock DBus objects from

You must be able to ssh into the machine as the file is downloaded in /tmp/

Step 2: Take the snapshot

Run the following command on the BMC

curl -X POST \
127.0.0.1/redfish/v1/Managers/bmc/ManagerDiagnosticData/Oem/Google/GoogleSnapShot/Actions/GoogleTakeSnapShot
cat /tmp/gBMCwebManagedStore.json | jq > $SNAPSHOT_FILE_NAME

Note: If you are getting an empty snapshot, that is most likely because you need to warm the cache. A quick BFS through all of the redfish URLs should warm the cache and after retrying these steps again, you should have a full serialization.

Step 3: Move it to the gbmcweb directory

You can add the snapshot to the folder “test/snapshots/”.

Please also update the readme file.

Step 4: Ensure the snapshot can be picked up by the presubmit in unit tests

You can create a new meson option to hardcode the snapshot filepath.

Please note that if you would like the presubmit to pass using your new snapshot, you will need to include the ci_workspace in the filepath.

For example, if my snapshot is located in test/snapshots/snapshot1.json, the default filepath of the snapshot should be /tmpfs/src/ci_workspace/gbmcweb/test/snapshots/snapshot1.json.

Step 5: Create a snapshot fixture to use in unit tests

You can now create a snapshot fixture that deserializes the json at the filepath you designed in Step #4. Then you can proceed to write the unit test as normal.