Don’t Make Me Think Principle, Testing, and Intuitive Expectations

A new extension to Minitest Expectations by yours truly is the perfect illustration for this philosophy of programming. #

A shorthand for how I think about the design of new APIs, whether I’m working on the Bridgetown web framework or another library, is a principle I’ve come to call Don’t Make Me Think. (DMMT)

DMMT plays out in many ways. Sometimes you might compare it to the Principle of Least Astonishment (or Surprise), but ultimately I think it’s more about the vibes, man and taste born of experience than any pat technical explanation. The aforementioned Wikipedia article also links to some other interesting and related principles such as Do What I Mean (DWIM) and that old chestnut Convention Over Configuration.

When it comes to software testing, I believe the DMMT principle needs to kick into overdrive. Let me set the context for my entreaty:

  1. I do not, have not, and will not practice TDD (Test-Driven Development). Does that shock you? I hope not. While I have no problem with people who are adherents to the TDD philosophy, I do have a massive problem with the notion that one must practice TDD to be a Good Programmer. Here’s what I do: I write tests when I need to write tests. Sometimes that’s before I work on a problem, in which case it looks like I’m practicing TDD. Other times I write tests after I’ve solved a problem. Still other times, I write no tests at all. Again, does that shock you? I hope not.
  2. I do not enjoy writing tests. I enjoy solving problems. Test-writing is a “side effect” of wanting to author robust and reliable code, just as dish-washing is a “side effect” of keeping your kitchen clean and useful for the real task of cooking and eating delicious food. So while I might sometimes tolerate a certain degree of fussiness and ceremony in the process of authoring user-focused code because the end results are worth it, I have no such tolerance in my authoring of tests. Get in, get green, get out as fast as humanly possible is my motto!

Given all that, I have spent the bulk of my career as a programmer wondering why some testing frameworks and techniques can be such a right PITA. Because if the primary goal is to get me to write more tests (a somewhat noble goal I might also poke holes in as the contrarian that I am), your testing framework is not doing its job!

From RSpec to Minitest Assertions to Expectations to Intuitive Expectations #

Like many Rubyists, my first memories of working on Ruby applications and writing tests was in relation to the RSpec testing framework. Now I’m not here to cast aspersions on the creators of RSpec nor its many fans. Let’s just say RSpec has never tickled my fancy.

I mean,

expect(actual).to be_within(delta).of(expected)

superficially looks like a cool Ruby DSL, and we all love a good DSL right? Unfortunately, RSpec routinely falls down pretty hard on the Don’t Make Me Think (DMMT) principle. I have to hold all of this syntax in my head:

Now it’s true I would probably not use this particular matcher very often, as I don’t typically work on math problems requiring these calculations. But I think it’s a good illustration for the point I’m making.

There’s also a lot about the RSpec code styles and ecosystem I don’t particularly care for. I don’t like all the indirections of multiple lets and how mocks look and just a whole host of issues. Again, if you personally dig all that, good for you! I always found it to be a miserable experience.

As time passed, I started to run into increased usage of Minitest and a breezy style of writing very simple test methods with a small set of possible assertions. Coming from RSpec, it’s almost unbearably terse, but once you get used to it, it’s hard to beat. For some cases like simply testing if something is true or false, it’s a revelation:

something_is = true
something_isnt = false

assert something_is
refute something_isnt

While that sort of syntax might not feel wildly Ruby-esque, I think it’s an amazing example of the DMMT principle.

However, as time passed I started to grow a bit frustrated with simply writing assertions all the time. Not only is there the strange “backwards” arguments issue of Minitest’s assert_equal with the expected rather than the actual coming first (not a design flaw of Minitest per se, it has a long history predating Minitest and even the Ruby community), I don’t particularly care for the way my tests look with dozens or hundreds of assert_* or refute_* statements. I also am a big fan of spec-style test writing with blocks of describe/it or context/should or however you want to define those terms, and often you find the spec-style tests written not with assertions but with “expectations”.

Minitest offers its own expectations syntax which I like to use with expect. The matchers all start with either must or wont:

expect(first_name).must_equal "Jared"
expect(last_name).wont_equal "Whyte"

It’s also quite nice when you need to run code within a block to match an expectation, such as checking if an exception was raised:

expect { crash_my_app! }.must_raise MyApp::CrashedException

Yet, me being me, it wasn’t long before I started to ask myself the following question: why do I need to think about all of these must_* and wont_* matchers? DMMT! I should be able to write the most standard, most obvious Ruby language code possible without needing to learn much of anything at all.

For example, if I wanted to check the equality of two values, couldn’t I simply write this?

expect(foo) == "bar"

And if not, why not?

With that question in mind, I set out to experiment with the most Don’t Make Me Think testing paradigm possible…and I succeeded. 😎

Enter Intuitive Expectations.

How Intuitive are Intuitive Expectations? Very Intuitive! #

A new feature provided by the Bridgetown Foundation gem (and one you can use in any Ruby app, not just a Bridgetown project), Intuitive Expectations builds upon Minitest’s exceptions and provides simpler and chainable variations.

Here are some examples from the docs:

expect(some_int) != 123
expect(some_big_str) << "howdy" # or expect(...).include? ...
expect(some_bool).true? # aliased to truthy?
expect(2..4).within?(1..6)
expect(food_tastes) =~ /g(r+)eat/

I also am a huge fan of chainable DSLs (aka where the methods return self so you can reuse that context), so naturally I had to make sure that works as well:

expect(big_string)
  .include?("foo")
  .include?("bar")
  .exclude?("baz")

expect(user)
  .is?(:moderator?)
  .isnt?(:admin?)

Most of the named matchers have a not twin, so it’s easy to intuit:

expect(beer_on_the_wall).match? /[0-9]+ bottles/
expect(wine_on_the_wall).not_match? /[0-9]+ bottles/

We’re really excited on the Bridgetown Core team to be moving away from an older testing style based originally on a fork of Jekyll using the Shoulda gem and Minitest assertions to Minitest spec-style and Intuitive Expectations (not in all but in many cases). Here’s an example excerpt from such a test:

class TestComponents < BridgetownUnitTest
  describe "Bridgetown::Component" do
    it "renders with captured block content" do
      # lots of funky whitespace from all the erb captures!
      spaces = "  "
      morespaces = "      "
      expect(@erb_page.output) << <<~HTML
        <app-card>
          <header>I&#39;M A CARD</header>
          <app-card-inner>
          #{spaces}
          <p>I'm the body of the card</p>

          #{morespaces}<img src="test.jpg" />

          #{spaces}NOTHING
          </app-card-inner>
          <footer>I&#39;m a footer</footer>
        </app-card>
      HTML
    end

    it "does not render if render? is false" do
      expect(@erb_page.output)
        .exclude?("NOPE")
        .exclude?("Canceled!")
    end

    it "handles same-file namespaced components" do
      expect(@erb_page.output) << "<card-section>blurb contents</card-section>"
    end
  end
end

Beauty is in the eye of the beholder and all art is subjective, but I truly believe Minitest Spec + Intuitive Expectations is the most elegant test syntax I’ve ever laid eyes on. Once this was available to me, I grew used to it so quickly I now feel like any project is “broken” if I can’t use this syntax. It’s so familiar to me that I quickly wrote a JavaScript version built on top of Node’s built-in assertions and testing features so I can write this in JavaScript:

content = await page.evaluate(() => document.querySelector("div").shadowRoot.innerHTML)
expect(content).include("<p>I'm in the shadow DOM!</p>")
expect(content).notMatch(/s\w*z/)

const funkyTown = await page.evaluate(() => document.documentElement.dataset.results)
expect(funkyTown).equal("won't you take me 2")

const count = await page.evaluate(() => document.querySelectorAll("#delete-me").length)
expect(count).notEqual(1)

I guess once you expect Intuitive Expectations, you never go back. small red gem symbolizing the Ruby language

Skip to content