| # 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](README_GOOGLE.md) |
| |
| ## Writing Unit Tests for GET Handlers |
| |
| Please take a look at this [example commit](https://gbmc-review.git.corp.google.com/c/gbmcweb/+/25255) 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](https://gbmc-review.git.corp.google.com/c/gbmcweb/+/25275) 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](https://gbmc-review.git.corp.google.com/c/gbmcweb/+/27074) and follow along. |
| |
| We want to test the URI “/redfish/v1/Systems/<str>/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](https://gbmc-review.git.corp.google.com/c/gbmcweb/+/25276) 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. |