summaryrefslogtreecommitdiff
path: root/chromium/docs/mojo_testing.md
blob: 95941a35d6e34d2744e11edc0b19d82773c0cc57 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# Testing With Mojo

This document outlines some best practices and techniques for testing code which
internally uses a Mojo service. It assumes familiarity with the
[Mojo and Services] document.

## Example Code & Context

Suppose we have this Mojo interface:

```mojom
module example.mojom;

interface IncrementerService {
  Increment(int32 value) => (int32 new_value);
}
```

and this C++ class that uses it:

```c++
class Incrementer {
 public:
  Incrementer();

  void SetServiceForTest(
      mojo::PendingRemote<mojom::IncrementerService> service);

  // The underlying service is async, so this method is too.
  void Increment(int32_t value,
                 IncrementCallback callback);

 private;
  mojo::Remote<mojom::IncrementerService> service_;
};

void Incrementer::SetServiceForTesting(
    mojo::PendingRemote<mojom::IncrementerService> service) {
  service_.Bind(std::move(service));
}

void Incrementer::Increment(int32_t value, IncrementCallback callback) {
  if (!service_)
    service_ = LaunchIncrementerService();
  service_->Increment(value, std::move(callback));
}
```

and we wish to swap a test fake in for the underlying IncrementerService, so we
can unit-test Incrementer. Specifically, we're trying to write this (silly) test:

```c++
// Test that Incrementer correctly handles when the IncrementerService fails to
// increment the value.
TEST(IncrementerTest, DetectsFailureToIncrement) {
  Incrementer incr;
  FakeIncrementerService service;
  incr.SetServiceForTest(service);

  // Incrementing is async, so we have to wait...
  base::RunLoop loop;
  int returned_value;
  incr.Increment(0,
    base::BindLambdaForTesting([&](int value) {
      returned_value = value;
      loop.Quit();
    }));
  loop.Run();

  EXPECT_EQ(0, returned_value);
}
```

## The Fake Service Itself

This part is fairly straightforward. Mojo generated a class called
mojom::IncrementerService, which is normally subclassed by
IncrementerServiceImpl (or whatever) in production; we can subclass it
ourselves:

```c++
class FakeIncrementerService : public mojom::IncrementerService {
 public:
  void Increment(int32_t value, IncrementCallback callback) override {
    // Does not actually increment, for test purposes!
    std::move(callback).Run(value);
  }
}
```

## Async Services

If we plug the FakeIncrementerService in in our test:

```c++
  mojo::Receiver<IncrementerService> receiver{&fake_service};
  incrementer->SetServiceForTest(receiver);
```

we can invoke it and wait for the response as we usually would:

```c++
  base::RunLoop loop;
  incrementer->Increment(1, base::BindLambdaForTesting(...));
  loop.Run();
```

... and all is well. However, we might reasonably want a more flexible
FakeIncrementerService, which allows for plugging different responses in as the
test progresses. In that case, we will actually need to wait twice: once for the
request to arrive at the FakeIncrementerService, and once for the response to be
delivered back to the Incrementer.

## Waiting For Requests

To do that, we can instead structure our fake service like this:

```c++
class FakeIncrementerService : public mojom::IncrementerService {
 public:
  void Increment(int32_t value, IncrementCallback callback) override {
    CHECK(!HasPendingRequest());
    last_value_ = value;
    last_callback_ = std::move(callback);
    if (wait_loop_)
      wait_loop_->Quit();
  }

  bool HasPendingRequest() const {
    return bool(last_callback_);
  }

  void WaitForRequest() {
    if (HasPendingRequest())
      return;
    wait_loop_ = std::make_unique<base::RunLoop>();
    wait_loop_->Run();
  }

  void AnswerRequest(int32_t value) {
    CHECK(HasPendingRequest());
    std::move(last_callback_).Run(value);
  }
};
```

That having been done, our test can now observe the state of the code under test
(in this case the Incrementer service) while the mojo request is pending, like
so:

```c++
  FakeIncrementerService service;
  mojo::Receiver<mojom::IncrementerService> receiver{&service};

  Incrementer incrementer;
  incrementer->SetServiceForTest(receiver);
  incrementer->Increment(1, base::BindLambdaForTesting(...));

  // This will do the right thing even if the Increment method later becomes
  // synchronous, and exercises the same async code paths as the production code
  // will.
  service.WaitForRequest();
  service.AnswerRequest(service.last_value() + 2);

  // The lambda passed in above will now asynchronously run somewhere here,
  // since the response is also delivered asynchronously by mojo.
```

## Test Ergonomics

The async-ness at both ends can create a good amount of boilerplate in test
code, which is unpleasant. This section gives some techniques for reducing it.

### Sync Wrappers

One can use the [synchronous runloop] pattern to make the mojo calls appear to
be synchronous *to the test bodies* while leaving them asynchronous in the
production code. Mojo actually generates test helpers for this already! We can
include `incrementer_service.mojom-test-utils.h` and then do:

```c++
int32_t Increment(Incrementer* incrementer, int32_t value) {
  int32_t result;
  mojom::IncrementerAsyncWaiter sync_incrementer(incrementer);
  sync_incrementer.Increment(value, &result);
  return result;
}
```

Note that this only works if FakeIncrementerService does not need to be told
when to send a response (via AnswerRequest or similar) - if it does, this
pattern will deadlock!

To avoid that, the cleanest approach is to have the FakeIncrementerService
either contain a field with the next expected value, or a callback that produces
expected values on demand, so that your test code reads like:

```c++
  service.SetNextValue(2);
  EXPECT_EQ(Increment(incrementer, 1), 2);
```

or similar.

[Mojo and Services]: mojo_and_services.md
[synchronous runloop]: patterns/synchronous-runloop.md