Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 1 | # Testing With Mojo |
| 2 | |
| 3 | This document outlines some best practices and techniques for testing code which |
| 4 | internally uses a Mojo service. It assumes familiarity with the |
| 5 | [Mojo and Services] document. |
| 6 | |
| 7 | ## Example Code & Context |
| 8 | |
| 9 | Suppose we have this Mojo interface: |
| 10 | |
| 11 | ```mojom |
| 12 | module example.mojom; |
| 13 | |
| 14 | interface IncrementerService { |
| 15 | Increment(int32 value) => (int32 new_value); |
| 16 | } |
| 17 | ``` |
| 18 | |
| 19 | and this C++ class that uses it: |
| 20 | |
| 21 | ```c++ |
| 22 | class Incrementer { |
| 23 | public: |
| 24 | Incrementer(); |
| 25 | |
Shuhai Peng | 376a0cb1 | 2022-10-19 23:57:50 | [diff] [blame] | 26 | void SetServiceForTesting( |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 27 | mojo::PendingRemote<mojom::IncrementerService> service); |
| 28 | |
| 29 | // The underlying service is async, so this method is too. |
| 30 | void Increment(int32_t value, |
| 31 | IncrementCallback callback); |
| 32 | |
| 33 | private; |
| 34 | mojo::Remote<mojom::IncrementerService> service_; |
| 35 | }; |
| 36 | |
| 37 | void Incrementer::SetServiceForTesting( |
| 38 | mojo::PendingRemote<mojom::IncrementerService> service) { |
| 39 | service_.Bind(std::move(service)); |
| 40 | } |
| 41 | |
| 42 | void Incrementer::Increment(int32_t value, IncrementCallback callback) { |
| 43 | if (!service_) |
| 44 | service_ = LaunchIncrementerService(); |
| 45 | service_->Increment(value, std::move(callback)); |
| 46 | } |
| 47 | ``` |
| 48 | |
| 49 | and we wish to swap a test fake in for the underlying IncrementerService, so we |
| 50 | can unit-test Incrementer. Specifically, we're trying to write this (silly) test: |
| 51 | |
| 52 | ```c++ |
| 53 | // Test that Incrementer correctly handles when the IncrementerService fails to |
| 54 | // increment the value. |
| 55 | TEST(IncrementerTest, DetectsFailureToIncrement) { |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 56 | Incrementer incrementer; |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 57 | FakeIncrementerService service; |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 58 | // ... somehow use `service` as a test fake for `incrementer` ... |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 59 | |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 60 | incrementer.Increment(0, ...); |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 61 | |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 62 | // ... Get the result and compare it with 0 ... |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 63 | } |
| 64 | ``` |
| 65 | |
| 66 | ## The Fake Service Itself |
| 67 | |
| 68 | This part is fairly straightforward. Mojo generated a class called |
| 69 | mojom::IncrementerService, which is normally subclassed by |
| 70 | IncrementerServiceImpl (or whatever) in production; we can subclass it |
| 71 | ourselves: |
| 72 | |
| 73 | ```c++ |
| 74 | class FakeIncrementerService : public mojom::IncrementerService { |
| 75 | public: |
| 76 | void Increment(int32_t value, IncrementCallback callback) override { |
| 77 | // Does not actually increment, for test purposes! |
| 78 | std::move(callback).Run(value); |
| 79 | } |
| 80 | } |
| 81 | ``` |
| 82 | |
| 83 | ## Async Services |
| 84 | |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 85 | We can plug the FakeIncrementerService into our test using: |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 86 | |
| 87 | ```c++ |
| 88 | mojo::Receiver<IncrementerService> receiver{&fake_service}; |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 89 | incrementer->SetServiceForTesting(receiver.BindNewPipeAndPassRemote()); |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 90 | ``` |
| 91 | |
| 92 | we can invoke it and wait for the response as we usually would: |
| 93 | |
| 94 | ```c++ |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 95 | base::test::TestFuture test_future; |
| 96 | incrementer->Increment(0, test_future.GetCallback()); |
| 97 | int32_t result = test_future.Get(); |
| 98 | EXPECT_EQ(0, result); |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 99 | ``` |
| 100 | |
| 101 | ... and all is well. However, we might reasonably want a more flexible |
| 102 | FakeIncrementerService, which allows for plugging different responses in as the |
| 103 | test progresses. In that case, we will actually need to wait twice: once for the |
| 104 | request to arrive at the FakeIncrementerService, and once for the response to be |
| 105 | delivered back to the Incrementer. |
| 106 | |
| 107 | ## Waiting For Requests |
| 108 | |
| 109 | To do that, we can instead structure our fake service like this: |
| 110 | |
| 111 | ```c++ |
| 112 | class FakeIncrementerService : public mojom::IncrementerService { |
| 113 | public: |
| 114 | void Increment(int32_t value, IncrementCallback callback) override { |
| 115 | CHECK(!HasPendingRequest()); |
| 116 | last_value_ = value; |
| 117 | last_callback_ = std::move(callback); |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 118 | if (!signal_.IsReady()) { |
| 119 | signal_->SetValue(); |
| 120 | } |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 121 | } |
| 122 | |
| 123 | bool HasPendingRequest() const { |
| 124 | return bool(last_callback_); |
| 125 | } |
| 126 | |
| 127 | void WaitForRequest() { |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 128 | if (HasPendingRequest()) { |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 129 | return; |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 130 | } |
| 131 | signal_.Clear(); |
| 132 | signal_.Wait(); |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 133 | } |
| 134 | |
| 135 | void AnswerRequest(int32_t value) { |
| 136 | CHECK(HasPendingRequest()); |
| 137 | std::move(last_callback_).Run(value); |
| 138 | } |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 139 | private: |
| 140 | int32_t last_value_; |
| 141 | IncrementCallback last_callback_; |
| 142 | base::test::TestFuture signal_; |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 143 | }; |
| 144 | ``` |
| 145 | |
| 146 | That having been done, our test can now observe the state of the code under test |
| 147 | (in this case the Incrementer service) while the mojo request is pending, like |
| 148 | so: |
| 149 | |
| 150 | ```c++ |
| 151 | FakeIncrementerService service; |
| 152 | mojo::Receiver<mojom::IncrementerService> receiver{&service}; |
| 153 | |
| 154 | Incrementer incrementer; |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 155 | incrementer->SetServiceForTesting(receiver.BindNewPipeAndPassRemote()); |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 156 | incrementer->Increment(1, base::BindLambdaForTesting(...)); |
| 157 | |
| 158 | // This will do the right thing even if the Increment method later becomes |
| 159 | // synchronous, and exercises the same async code paths as the production code |
| 160 | // will. |
| 161 | service.WaitForRequest(); |
| 162 | service.AnswerRequest(service.last_value() + 2); |
| 163 | |
| 164 | // The lambda passed in above will now asynchronously run somewhere here, |
| 165 | // since the response is also delivered asynchronously by mojo. |
| 166 | ``` |
| 167 | |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 168 | ## Intercepting Messages to Bound Receivers |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 169 | |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 170 | In some cases, particularly in browser tests, we may want to take an existing, |
| 171 | bound `mojo::Receiver` and intercept certain messages to it. This allows us to: |
| 172 | - modify message parameters before the message is handled by the original |
| 173 | implementation, |
| 174 | - modify returned values by intercepting callbacks, |
| 175 | - introduce failures, or |
| 176 | - completely re-implement the message handling logic |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 177 | |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 178 | To accomplish this, Mojo autogenerates an InterceptorForTesting class for each |
| 179 | interface that can be subclassed to perform the interception. Continuing with |
| 180 | the example above, we can include `incrementer_service.mojom-test-utils.h` and |
| 181 | then use the following to intercept and replace the number to be incremented: |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 182 | |
| 183 | ```c++ |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 184 | class IncrementerServiceInterceptor |
| 185 | : public mojom::IncrementerServiceInterceptorForTesting { |
| 186 | public: |
| 187 | // We'll assume RealIncrementerService implements the Mojo interface, owns the |
| 188 | // the bound mojo::Receiver, and makes it available to use via a testing |
| 189 | // method we added named `receiver_for_testing()`. |
| 190 | IncrementerServiceInterceptor(RealIncrementerService* service, |
| 191 | int32_t value_to_inject) |
| 192 | : service_(service), |
| 193 | value_to_inject_(value_to_inject), |
| 194 | swapped_impl_(service->receiver_for_testing(), this) {} |
| 195 | |
| 196 | ~IncrementerServiceInterceptor() override = default; |
| 197 | |
| 198 | mojom::IncrementerService* GetForwardingInterface() |
| 199 | override { |
| 200 | return service_; |
| 201 | } |
| 202 | |
| 203 | void Increment(int32_t value, |
| 204 | IncrementCallback callback) override { |
| 205 | GetForwardingInterface()->Increment(value_to_inject_, std::move(callback)); |
| 206 | } |
| 207 | |
| 208 | private: |
| 209 | raw_ptr<RealIncrementerService> service_; |
| 210 | int32_t value_to_inject_; |
| 211 | mojo::test::ScopedSwapImplForTesting< |
| 212 | mojo::Receiver<mojom::IncrementerService>> |
| 213 | swapped_impl_; |
| 214 | }; |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 215 | ``` |
| 216 | |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 217 | ## Ensuring Message Delivery |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 218 | |
Andrew Williams | 7b42945 | 2023-08-01 17:31:20 | [diff] [blame] | 219 | Both `mojo::Remote` and `mojo::Receiver` objects have a `FlushForTesting()` |
| 220 | method that can be used to ensure that queued messages and replies have been |
| 221 | sent to the other end of the message pipe, respectively. `mojo::Remote` objects |
| 222 | also have an asynchronous version of this method call `FlushAsyncForTesting()` |
| 223 | that accepts a `base::OnceCallback` that will be called upon completion. These |
| 224 | methods can be particularly helpful in tests where the `mojo::Remote` and |
| 225 | `mojo::Receiver` might be in separate processes. |
Elly Fong-Jones | 99516e2e | 2021-05-13 21:01:41 | [diff] [blame] | 226 | |
| 227 | [Mojo and Services]: mojo_and_services.md |