docs: add mojo testing sketch
Change-Id: Ie449ab12891f60d70db097cce4542c0201132dae Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2891199 Reviewed-by: Ken Rockot <rockot@google.com> Commit-Queue: Elly Fong-Jones <ellyjones@chromium.org> Cr-Commit-Position: refs/heads/master@{#882696}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
8c1742fe6d
commit
99516e2e69
206
docs/mojo_testing.md
Normal file
206
docs/mojo_testing.md
Normal file
@ -0,0 +1,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
|
Reference in New Issue
Block a user