The Dialect Diary – Assertion Generators

As part of Dialect’s operation, I want page and activity definitions to be able to assert aspects of themselves. This is different from the “assertion” you might be used to in terms of testing logic. This post will cover what assertion definitions mean and get into one of the key design elements of Dialect: the generator.

For this post, I’ll be considering the logic in the feature/assertion-generators branch.

Assertion definitions are methods that can be called directly on a page or activity definition. These methods assert some aspect of the page or activity. A method (or two) will be generated by Dialect when an assertion definition is encountered. That method will take some specific action based on the type of assertion. As I mentioned, this is the start of the generator concept, where methods are generated as part of Dialect’s basic operation. These assertion aspects seemed like the simplest place to start.

Let’s consider part of the script that I wrote in the evaluators post. Here I’m going to change it a bit to indicate how I would like things to work:

Here I’ve commented out certain lines from the previous script and added how I would like those to be used. As a quick side-note, notice how I commented out the rspec lines as well? I did that because I quickly decided I would want to use RSpec — and thus include the Matchers module — in just about every test script. As such, to reduce the boilerplate, I decided to incorporate the require of rspec and the inclusion of the Matchers into Dialect itself.

So the test framework … has a test framework?

Actually, no, it does not. Not really, anyway. So far what I’ve done will bring RSpec into the page and activity definitions — but it will not allow RSpec matchers to be used in the top-level script itself. I’ll come back to that point later. Of perhaps more importance, you might wonder why I’m doing this. After all, this means Dialect now has a dependency on RSpec. That also means Dialect is, in one sense, opinionated. It promotes RSpec as part of its operation, although it certainly does not demand its use. Is that bad or good? Someone could certainly use another expectations test framework with Dialect and not use RSpec at all. Dialect doesn’t stop them so I haven’t limited people’s options. But have I bloated Dialect just to satisfy my own opinionated approach? Honestly, I don’t know. I do know that one thing I like is test frameworks that reduce boilerplate code, which this has helped me do a little. Beyond that, including support for RSpec, which is hugely popular and supported, does seem like a good thing to me.

Moving on to the substantive changes to that test script above, you can see that I would like there to be a view() method. When that method is called on a page definition, the browser will be instructed to go to the URL for the page that the page definition represents. Based on the above test script, I would then like check_url() and check_title() methods that do exactly that: check the URL and title of the currently rendered page.

Up to this point, we’ve done very little with the page definition itself. That’s going to change from this point forward, albeit slowly.

Asserting on the Page Definition

Let’s consider the view() method first. Notice that with the previous visit() method I was using in the test script, the evaluator literally “evaluated” where to visit based on the argument passed in, which was the URL. The view() method, however, takes no such argument. So how does it know where to go? Well, the action of getting the appropriate URL is now delegated to the one place that should know about the URL: the page definition itself. In other words, a page should know how to get to itself and thus Dialect must expose that as part of the API.

And that brings up the API: how do I let someone specify the url on the page definition? Clearly I could just do an instance variable and use an accessor or something like that. But I want this API to be expressive with a minimum of programmatic construct. Further, when the appropriate API method is used, I want certain methods to be generated as a result of that use. Then, once generated, those methods can be used in the test script. A key point is that if that part of the API is not used, however, those methods will not be generated.

What this means, as you can probably see, is that Dialect is capable of building itself as it executes based on what it finds with your test scripts. I think that kind of thing is one of the key strengths of a dynamic language like Ruby or Python. It’s why I find languages like Java so limiting, at least for test solution development.

First notice lines 14-15 and 21 of dialect.rb. You’ll see that I’ve included Dialect::Evaluator and Dialect::Platform, but I have extended Dialect::Generator::Assertion. Why the difference? With include I have allowed an instance of a page definition (which includes Dialect) to call aspects of Dialect itself. Those aspects can be called on an instance of a Dialect class. By extending aspects of Dialect into the included class, I’m allowing the included class (not an instance of it) to directly call aspects of Dialect. This is important because the class will define, while the instances of the class will then use what is defined. Put simply, include makes the module’s methods avaiable to the instance of the class page definition while extend makes those methods available on the class itself.

Now consider assertions.rb. Here I have a method on Dialect::Generator::Assertion called url_is(). Because this module is extended into the calling class, this method can be used directly in that class. So my page definition becomes:

Now notice that within the url_is() method, there is a call to Ruby’s define_method(). That is what, in fact, defines the view() method and it is thismethod that you can then use to visit the URL that is specified by the url_is assertion definition. Finally, to tie this all together, yo might notice that the logic within the defined (or generated) view() method delegates to the visit() method on the platform. This shows Dialect using an evaluator method (visit) that was introduced in the discussion about the Evaluator module and that is being called on the platform object that was introduced in the Platform module.

You’ll also note that a check_url() method is generated as well. Again, these methods are generated when someone uses a url_is() method call — assertion definition — on their page or activity definition. I call these “assertion definitions” largely because you are asserting that something is the case: i.e., that the page has a particular URL. I’ve personally found that test frameworks should have definite names for constructs, even if, in reality, that construct is nothing more than just a method call.

Make sure you understand how the above works because it’s essentially the exact same for handling the title. There is an assertion method called title_is() that you can set on your page definition as such:

If you look at the module in assertions.rb, you’ll see that it generates a check_title() method.

Expectations: Back to RSpec

Take a quick look back at that test script I started this post with. Now consider that both check_url and check_title will automatically perform checking behind the scenes. So statements like the following (which I said I would want to support) are not necessary:

In fact, you might notice that with the way I implemented check_url and check_title, those expectations would not even work. They would instead have to be:

And now something else rears its head and it involves RSpec. The ‘be_true’ part of the above examples will not be recognized at the top-level of the script since I commented out RSpec::Matchers. Remember I said I’d come back to this? Well, here we are. If I did want to use phrases like the above, I would have to put that line back in at the top of the script. Do note, however, that I would not have to require RSpec since Dialect does that.

While on the subject, take another look at my code in assertions.rb. You’ll see that I do some checking there to determine whether the URL and the title are valid, based on the expectations of what was asserted. You’ll also see that I’m not using RSpec to do any checking there; I’m using basic equality checks and nothing more. Is that good or bad? After all, I incorporated RSpec into Dialect. Isn’t it a shame to not use it? It would seem I’m not using RSpec at a point where it be most beneficial, right? Well, the problem is that right now I require and include RSpec so that it reduces boilerplate in test script logic that is using Dialect. That’s a pretty loose dependency. If I start tying how Dialect works with RSpec, that’s a harder dependency.

So my approach here seems like a wise move to me, at least with what I know now.

Back to the Script

I know I’ve jumped around a bit so let’s consider what a potential test script looks like right now:

You can see the unit tests on assertions to see that this stuff works but if you wanted to check if an incorrect URL would be flagged, just force a visit to another page between the view and the check. For example:

The title is easier to check: just put an incorrect title for the title_is assertion.

A few things you can glean from the unit tests: Dialect will handle if the assertions are declared but not defined. Meaning, for example, someone puts in a url_is or title_is assertion call but passes nothing to either. That’s probably not the likeliest of scenarios but you never know.

Dialect will also handle if you make calls to view, check_url, or check_title but a url_is and/or title_is assertion is not provided. That’s probably a more likely scenario since it could be easy to forget them. However, handling this got tricky and led to what might be an interesting design choice. And by “interesting” I mean problematic.

Any (Missing) Method Will Do

The problem is that view, check_url and check_title are generated methods. So if url_is is not used, for example, then view() is never created. This led me to doing something that I might consider controversial, at least in terms of being a wise decision. I want to allow Dialect to direct how people use it. I don’t just want to present errors like this:

$ /lib/ruby/gems/2.0.0/gems/rspec-expectations-2.14.5/lib/rspec/matchers/method_missing.rb:9:in
  method_missing': undefined method view' for #<DialectTest:0x255f210> (NoMethodError)
  from ./AppDev/dialect-test.rb:13:in `<main>'

That’s pretty much what you would get if you used “page.view” and did not define url_is on your page definition. So what I did was add a missing_method() method in the errors.rb module. The handles the cases where someone uses the view, check_url, and check_title methods but those methods have not been generated because they have not used the assertion methods.

That allows me to handle a lot of potential issues — but it may potentially open up a lot of other ones. After all, now any method that is called on a page/activity definition instance will be “handled” by missing_method. I know this is a bit like playing with a gasoline-drenched sword in one hand and a lit match in the other, but I feel the risk is worth it given the design direction I have for the framework.

Moving Forward…

At this point I’ve proven out (to myself, anyway) the viability of the platform object as well as the notion of generators. Both of those are now going to be key in the continued evolution as the framework as I work out how to deal with the web elements that the framework is designed to work with.


This article was written by Jeff Nyman

Anything I put here is an approximation of the truth. You're getting a particular view of myself ... and it's the view I'm choosing to present to you. If you've never met me before in person, please realize I'm not the same in person as I am in writing. That's because I can only put part of myself down into words. If you have met me before in person then I'd ask you to consider that the view you've formed that way and the view you come to by reading what I say here may, in fact, both be true. I'd advise that you not automatically discard either viewpoint when they conflict or accept either as truth when they agree.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.