blob: 67c437e0dd97e732863beb04d1b0d5972b9d9f14 [file] [log] [blame] [view]
# 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 SetServiceForTesting(
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) {
mojom::IncrementerAsyncWaiter sync_incrementer(incrementer);
return sync_incrementer.Increment(value);
}
```
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