Test frameworks
Optional helpers for test suites in pytest, Node.js and .NET: a phone fixture, the phone's recent commands in failure reports, a screenshot and screen tree for each failed test, and a phone per parallel worker.
Any test runner can call Droidline as it is. The helpers on this page are optional and add what people usually end up writing by hand: a fixture that hands each test the phone, a log of what the test sent to the phone, and the screen at the moment a test failed. Nothing here loads unless you turn it on.
pytest
Turn the plugin on for one run, or for the project in conftest.py:
pytest -p droidline.pytest_plugin
# conftest.py
pytest_plugins = ["droidline.pytest_plugin"]
Then ask for the phone fixture:
def test_log_in(phone):
phone.launch("com.example.shop")
phone.input("id", "email", "knife@example.com")
phone.touch("text", "Log in")
assert phone.exists("id", "greeting")
| Option | What it does |
|---|---|
--droidline-device NAME | The phone to test on. Without it: DROIDLINE_DEVICE, else the only online phone. |
--droidline-lease | Lease a free phone for the session. With pytest -n 4 (pytest-xdist) each worker gets its own phone, waiting up to 30 seconds for one to be free. |
--droidline-artifacts DIR | For every failed test, save the phone's screenshot, screen tree and command log in DIR/<test>/. |
When a test that uses phone fails, its report gets a droidline commands section with the last requests it sent, so you can see the step that went wrong:
---------------------------- droidline commands ----------------------------
14:02:11 launch {"package": "com.example.shop"} -> ok (812 ms)
14:02:13 touch {"by": "text", "value": "Log in"} -> NOT_FOUND (10013 ms)
With --droidline-artifacts, the folder holds commands.txt, screen.json and screen.png. Each part is saved if it can be: an offline phone still leaves its command log, and on Android 9 and 10 the screenshot needs screen capture permission.
The plugin also provides droidline_client, one connection to the server for the whole session.
Other Python test runners
The same helpers work without pytest:
from droidline.testing import format_history, save_failure
try:
run_steps(d)
except Exception:
save_failure(d, "artifacts/checkout") # screenshot, tree and command log
raise
print(format_history(d.history[-10:])) # the last ten requests as text
Node.js
The helpers live in droidline/testing. Call saveFailure from wherever your runner tells you a test failed. With the built-in node:test:
import { after, test } from "node:test";
import { connect } from "droidline";
import { saveFailure } from "droidline/testing";
const d = await connect();
after(() => d.close());
// Runs a test body and keeps the evidence when it throws.
async function onPhone(t, body) {
try {
await body();
} catch (err) {
await saveFailure(d, `artifacts/${t.name}`);
throw err;
}
}
test("log in", (t) =>
onPhone(t, async () => {
await d.launch("com.example.shop");
await d.touch("text", "Log in");
}));
In Vitest or Jest, call saveFailure(d, folder) from your own failure hook. formatHistory(d.history) gives the command log as text.
.NET
There is no plugin package for .NET. The Testing class in the Droidline package has the same helpers, and you call them from your test framework. With xUnit:
using System.Runtime.CompilerServices;
using Droidline;
using Xunit;
// One phone for all the tests in the class.
public sealed class PhoneFixture : IAsyncLifetime
{
public Device Phone { get; private set; } = null!;
public async Task InitializeAsync() => Phone = await DroidlineClient.ConnectAsync();
public async Task DisposeAsync() => await Phone.DisposeAsync();
}
public class ShopTests : IClassFixture<PhoneFixture>
{
private readonly Device d;
public ShopTests(PhoneFixture fixture) => d = fixture.Phone;
// Runs a test body and keeps the evidence when it throws.
private async Task OnPhone(Func<Task> body, [CallerMemberName] string test = "")
{
try
{
await body();
}
catch
{
await Testing.SaveFailureAsync(d, $"artifacts/{test}");
throw;
}
}
[Fact]
public Task LogIn() => OnPhone(async () =>
{
await d.LaunchAsync("com.example.shop");
await d.TouchAsync("text", "Log in");
});
}
[CallerMemberName] fills in the name of the test method, so each failed test gets its own folder. In NUnit or MSTest, wrap the test bodies the same way. Testing.FormatHistory(d.History) gives the command log as text, for example to write it to xUnit's ITestOutputHelper.
xUnit runs test classes in parallel. To give each class its own phone, use DroidlineClient.LeaseAsync(wait: 60) instead of ConnectAsync() in InitializeAsync; disposing the phone gives it back.
The command log
Every client keeps its last 200 requests. d.history lists the ones for that phone, oldest first, each with cmd, params, ok, error and ms. In .NET it is d.History, with Cmd, Params, Ok, Error and Ms. Long values such as screenshots are shortened to their length. The log stays in your process; nothing is sent anywhere.
Several phones at once
Run tests in parallel and give each worker its own phone with leases: pytest -n 4 --droidline-lease, or lease() in your own setup (DroidlineClient.LeaseAsync() in .NET). See Share phones between scripts.