a Rug story: adding test cases

These days I work on Rug, Atomist’s library for coding code modifications.

Adding a feature, I start by creating a test. While it’s tempting to create a narrow test around the piece of code I want to change, it’s better to create an API-level test. Testing at the outside has a few benefits: it tells the story of why this feature is needed; it drives pleasing API design; and it places minimum constraints on the implementation. The cost is, it’s more work.

The API level of Rug is in TypeScript, where people write programs to modify other programs. The test compiles the TypeScript to JavaScript, and rug executes that inside the JVM, where our Scala code does the tricky work of implementing the Rug programming model — navigating the project, parsing code, and making atomic modifications (it all works or none of it is saved). This means that my API-level tests include a TypeScript file and a Scala file, plus a bunch of wiring to hook them together. I get tired of remembering how to do this. Plus, we’re constantly improving the programming model in TypeScript, so “the right way” is a shifting target.

Last year, I would have copied an existing test (which one is up to date? I don’t know! guess and hope it works), modified parts of it for my needs (and forgotten some), embedded the TypeScript code as a string in Scala (seems easier than making a .ts file), and tried to abstract away some of the repetitive bits that are shared between tests (even though that obscures the storyline of the test).

This year, I have a new tool. About the third time I needed a new test, I wrote a program to create it for me. I wrote a Rug! My AddTypeScriptTest Rug editor creates a new TypeScript file in test/resources, and a new Scala file in test/scala. It bases these off of sample files that exemplify the current standard in Rugs and their tests, performing all the modifications that I mess up in the copy-paste-modify strategy.

me:

rug edit -l AddTypeScriptTest class_under_test=com.atomist.rug.NewFeature

my Rug program:

  • copies SampleTypeScriptTest.scala to a new location. Changes the package name, the class name, and the location of the TypeScript file it will load.
  • copies SampleTypeScriptTest.ts to a new location. Changes the name of the class and the exported instance.

SampleTypeScriptTest.scala and SampleTypeScriptTest.ts form a real test in rug’s test suite, so I know that my baseline continues to work. When I update the style of them (as I did today), I can run the sample test to be sure it works (caught two errors today). I maximize their design to best tell the story of how rug goes from a TypeScript file to a Rug archive to running that program on a separate project and seeing the results. This helps people spinning up on Rug understand it. Repetition (of the Scala package name and the path to the test program, for instance) doesn’t hurt because a program is modifying them consistently (bonus: IntelliJ will ctrl-click into the referenced file on the classpath. It didn’t when that repetition was abstracted). If I want to change the way all these tests work, I can do that with a Rug editor too, since they’re consistent. Ahhhh the consistency: when a test breaks, and it looks exactly like the other tests except for meaningful differences, debugging is easier.

I created this Rug editor inside the rug project itself, since it’s only relevant to this particular project. Then I run the rug CLI in local mode, on the local project, and poof. I’ve used rug to modify rug using a Rug inside rug. Super meta! (It doesn’t have to be so incestuous. Other days, I use rug to modify any project using a Rug in any Rug archive.)

If you want to create a Rug to automate your own frequent tasks, install the Rug CLI and, from your project root, use this Rug: rug edit atomist-rugs:rug-editors:AddLocalEditor editorName=WhatDoYouWantToCallIt . Find your starting point in .atomist/editors/WhatDoYouWantToCallIt.ts

Pop into Atomist community slack with questions and we will be soooo happy to help you.