blob: 077ed082381b4c0ec01a970146148d75e0ee9c3d [file] [log] [blame] [view]
Elly Fong-Jones99516e2e2021-05-13 21:01:411# Testing With Mojo
2
3This document outlines some best practices and techniques for testing code which
4internally uses a Mojo service. It assumes familiarity with the
5[Mojo and Services] document.
6
7## Example Code & Context
8
9Suppose we have this Mojo interface:
10
11```mojom
12module example.mojom;
13
14interface IncrementerService {
15 Increment(int32 value) => (int32 new_value);
16}
17```
18
19and this C++ class that uses it:
20
21```c++
22class Incrementer {
23 public:
24 Incrementer();
25
Shuhai Peng376a0cb12022-10-19 23:57:5026 void SetServiceForTesting(
Elly Fong-Jones99516e2e2021-05-13 21:01:4127 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
37void Incrementer::SetServiceForTesting(
38 mojo::PendingRemote<mojom::IncrementerService> service) {
39 service_.Bind(std::move(service));
40}
41
42void Incrementer::Increment(int32_t value, IncrementCallback callback) {
43 if (!service_)
44 service_ = LaunchIncrementerService();
45 service_->Increment(value, std::move(callback));
46}
47```
48
49and we wish to swap a test fake in for the underlying IncrementerService, so we
50can 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.
55TEST(IncrementerTest, DetectsFailureToIncrement) {
Andrew Williams7b429452023-08-01 17:31:2056 Incrementer incrementer;
Elly Fong-Jones99516e2e2021-05-13 21:01:4157 FakeIncrementerService service;
Andrew Williams7b429452023-08-01 17:31:2058 // ... somehow use `service` as a test fake for `incrementer` ...
Elly Fong-Jones99516e2e2021-05-13 21:01:4159
Andrew Williams7b429452023-08-01 17:31:2060 incrementer.Increment(0, ...);
Elly Fong-Jones99516e2e2021-05-13 21:01:4161
Andrew Williams7b429452023-08-01 17:31:2062 // ... Get the result and compare it with 0 ...
Elly Fong-Jones99516e2e2021-05-13 21:01:4163}
64```
65
66## The Fake Service Itself
67
68This part is fairly straightforward. Mojo generated a class called
69mojom::IncrementerService, which is normally subclassed by
70IncrementerServiceImpl (or whatever) in production; we can subclass it
71ourselves:
72
73```c++
74class 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 Williams7b429452023-08-01 17:31:2085We can plug the FakeIncrementerService into our test using:
Elly Fong-Jones99516e2e2021-05-13 21:01:4186
87```c++
88 mojo::Receiver<IncrementerService> receiver{&fake_service};
Andrew Williams7b429452023-08-01 17:31:2089 incrementer->SetServiceForTesting(receiver.BindNewPipeAndPassRemote());
Elly Fong-Jones99516e2e2021-05-13 21:01:4190```
91
92we can invoke it and wait for the response as we usually would:
93
94```c++
Andrew Williams7b429452023-08-01 17:31:2095 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-Jones99516e2e2021-05-13 21:01:4199```
100
101... and all is well. However, we might reasonably want a more flexible
102FakeIncrementerService, which allows for plugging different responses in as the
103test progresses. In that case, we will actually need to wait twice: once for the
104request to arrive at the FakeIncrementerService, and once for the response to be
105delivered back to the Incrementer.
106
107## Waiting For Requests
108
109To do that, we can instead structure our fake service like this:
110
111```c++
112class 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 Williams7b429452023-08-01 17:31:20118 if (!signal_.IsReady()) {
119 signal_->SetValue();
120 }
Elly Fong-Jones99516e2e2021-05-13 21:01:41121 }
122
123 bool HasPendingRequest() const {
124 return bool(last_callback_);
125 }
126
127 void WaitForRequest() {
Andrew Williams7b429452023-08-01 17:31:20128 if (HasPendingRequest()) {
Elly Fong-Jones99516e2e2021-05-13 21:01:41129 return;
Andrew Williams7b429452023-08-01 17:31:20130 }
131 signal_.Clear();
132 signal_.Wait();
Elly Fong-Jones99516e2e2021-05-13 21:01:41133 }
134
135 void AnswerRequest(int32_t value) {
136 CHECK(HasPendingRequest());
137 std::move(last_callback_).Run(value);
138 }
Andrew Williams7b429452023-08-01 17:31:20139 private:
140 int32_t last_value_;
141 IncrementCallback last_callback_;
142 base::test::TestFuture signal_;
Elly Fong-Jones99516e2e2021-05-13 21:01:41143};
144```
145
146That 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
148so:
149
150```c++
151 FakeIncrementerService service;
152 mojo::Receiver<mojom::IncrementerService> receiver{&service};
153
154 Incrementer incrementer;
Andrew Williams7b429452023-08-01 17:31:20155 incrementer->SetServiceForTesting(receiver.BindNewPipeAndPassRemote());
Elly Fong-Jones99516e2e2021-05-13 21:01:41156 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 Williams7b429452023-08-01 17:31:20168## Intercepting Messages to Bound Receivers
Elly Fong-Jones99516e2e2021-05-13 21:01:41169
Andrew Williams7b429452023-08-01 17:31:20170In some cases, particularly in browser tests, we may want to take an existing,
171bound `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-Jones99516e2e2021-05-13 21:01:41177
Andrew Williams7b429452023-08-01 17:31:20178To accomplish this, Mojo autogenerates an InterceptorForTesting class for each
179interface that can be subclassed to perform the interception. Continuing with
180the example above, we can include `incrementer_service.mojom-test-utils.h` and
181then use the following to intercept and replace the number to be incremented:
Elly Fong-Jones99516e2e2021-05-13 21:01:41182
183```c++
Andrew Williams7b429452023-08-01 17:31:20184class 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-Jones99516e2e2021-05-13 21:01:41215```
216
Andrew Williams7b429452023-08-01 17:31:20217## Ensuring Message Delivery
Elly Fong-Jones99516e2e2021-05-13 21:01:41218
Andrew Williams7b429452023-08-01 17:31:20219Both `mojo::Remote` and `mojo::Receiver` objects have a `FlushForTesting()`
220method that can be used to ensure that queued messages and replies have been
221sent to the other end of the message pipe, respectively. `mojo::Remote` objects
222also have an asynchronous version of this method call `FlushAsyncForTesting()`
223that accepts a `base::OnceCallback` that will be called upon completion. These
224methods can be particularly helpful in tests where the `mojo::Remote` and
225`mojo::Receiver` might be in separate processes.
Elly Fong-Jones99516e2e2021-05-13 21:01:41226
227[Mojo and Services]: mojo_and_services.md