Anatomy of a Test
3 points by Johz
3 points by Johz
Ha, I happen to have exactly this test for my secret side project, so we can compare notes!
test "basic job scheduling" {
var env: TestEnv = undefined;
env.init(.{});
defer env.deinit();
try env.pr_create(.{});
try env.merger_run_once();
try env.worker_run_once_async();
try env.expect_jobs(snap(@src(),
\\1 565070d q refs/pull/1/head
\\
));
try env.pipe.rendezvous();
try env.expect_jobs(snap(@src(),
\\1 565070d r refs/pull/1/head
\\
));
try env.pipe.rendezvous();
try env.worker_run_once_await();
try env.expect_jobs(snap(@src(),
\\1 565070d t refs/pull/1/head
\\
));
}
setUp / tearDown hooks, popularized by xUnit, but this is the case of patterning over a language deficiency. We got using in JavaScript like yesterday, and Java's try-with-resources in Java 7, well after the setup/teardown pattern was established. Before that, we only had try-finally, and it's a poor fit for tests, as it doesn't allow for nice composition of several resources (as you'll have to nest things). Absolutely no need to use that these days, just use your language's built-in idiom for managing resources, be it RAII, using or with. Honorable mention of pytest's auto-magic fixtures though, as they are sweet (but, again, it's a problem with with that it eats an indentation level).TestEnv object that does all of:
undefined stuff is just a Zig idiom for in-place initialization, which I like for pointer stability, not something you'd have to / be able to think about in a higher level language.act things as well. Furthermore, in this case the system under test is multiprocess, so I have to have a helper for shelling out and capturing the output.--testing output to the list jobs command. While normally the listing includes timestamps, they are elided for tests. I do love to control time, but this thing here is multiprocess, and I deliberately decided that I want to test the real thing, and mocking clocks across processes is tricky.queued, running, finished with true). And this is also part of test driven design, because this forces the whole system to be passable and resumable, which means that it can be safely killed and restarted at any moment, which is not something you get for free but as a result of carefully adjusting consistency semantics to make it possible.pipe2 trickery to pause subordinate processes when needed.Yeah, I've seen you talk before about preferring to test end-to-end where possible, rather than just the individual modules, and I think it's probably the best approach when it works. I suspect I rarely do that because I mostly do web development, and testing GUIs end-to-end is typically finicky and brittle. Passing inputs programmatically to a CLI, and reading the outputs (even if the outputs are formatted especially with a flag), is significantly easier than interacting programmatically with a complex DOM structure. Hence the preference for testing at least one layer down, so that the UI isn't as involved.
Having the act step also be managed the TestEnv object feels a lot like the Page Object pattern, and it makes sense if you're working at a higher level that you'd want to wrap all the fiddly details of the real environment/browser. I think I'm always a bit scared with page objects that they feel a bit like mocks, and I'll end up in a situation where the test looks reasonable, but underneath it's just mocks calling other mocks and nothing actually gets done. So I use them a bit, but often explicitly leave out the act bit so that the test feels a bit more real, if that makes sense? I should explore this more though.
I saw your comment recently that mentioned snapshot testing with the reference to Jane Street(?) and their setup where they have a specific textual representation of some system they wanted to test, which is specifically designed for snapshots. That sounds like what you're doing here — having a special --testing format that can be snapshotted more easily without cruft like timestamps. I've done this kind of thing while writing parsers before, because it's easier to test "does this parse tree look right" when a human can easily glance at the parse tree and get a feel for what it's showing.
There is definitely something to this idea of co-design and figuring out a design for the API/CLI/whatever that is also very testable. Here you mention resumability, I find the other big one is cancellability and cleanup, because that's really important for a test where you're starting and stopping things over and over again in very short timescales. But it's not just that testing makes you think about these things, it's that testing forces you to design so that these things are possible in the first place.
String test names are definitely <3, but so are Zig's multiline strings for snapshot testing — writing them like that looks so much clearer than having trailing quotation marks all over the place and feeling like the indentation is all weird!
a complex DOM structure
Yeah, if I were building a web app, I'd be snapshotting JSON API (if that's an SPA) or the dict with data I hand over to my template, if I am rendering server-side. I guess, snapshot testing view model is the educated way to put that?