Micro-Framework Communication Patterns

In my last post on micro-frameworks, I got into the organizing principles of my Tapestry solution, by which the framework provides or supports a mechanism for the encapslation of and delegation to logic. Here I’m going to continue on that theme but with a focus on showing how the framework calls into the tests, rather than the reverse, and why I think this is a good design approach.

A quick note: As with the previous post, this post will require an update to your Tapestry version, specifically to version 0.6.0. You can grab that via gem update tapestry. Again, part of this is me showing how design evolves as you attempt to explain and/or document something. Key to that, however, is making sure that you don’t negate all of the material you have already documented.

Let’s start with this script which is what we ended up with at the end of the last post:

As I mentioned in that previous post, the above is a little bit busy. Part of that “busy” aspect is that the script is conflating various aspects of interaction into one definition, called Veilus. Those of you who have used page objects already know what the solution to that is. But let’s take things step by step.

Delegate More To The Definitions

Notice how we’ve carried along those two lines to move the browser and maximize it, currently lines 27 and 28. Do I really want every test script to have to do that? Not really. But if I delegate that to the page object, what method would I call to make that happen? After all, a page object may store different methods that are called at different times depending on the test being executed.

What I need is a method that is always run. Tapestry provides such a method as a hook. That method is called begin_with. If this method exists on your definition, it will be called. So let’s try this out. Change the Veilus definition accordingly:

The following lines can now be removed from the test script:

You should find everything works as before.

Framework Calls Into Tests

What you should notice here is that while your test script can be thought of as calling into Tapestry — i.e., using the framework — what is in fact happening is that Tapestry is using your test script as an interface. In other words, the micro-framework calls into any tests that include it.

Consider that Tapestry was included in the page definition. Then you defined element definitions. But those weren’t calling methods on the micro-framework. Rather, those elements served as hooks that, if Tapestry found them, it would recognize them as methods for you. Then your script would simply be calling the methods that you yourself wrote. Tapestry just served as a sort of connective glue.

The same applies with the “begin_with” declaration above. Tapestry will always look to see if this method exists on a definition. And if it does, it will call it and do whatever it asks for. If that method isn’t there, well, then it isn’t there! That’s it. So Tapestry, as any test micro-framework should do (in my opinion), is allowing your logic to define the parameters by which execution will take place. Tapestry simply provides an API to make that possible.

Let’s see this in action in another context.

Basic Checks

You’ll note that I check for that ‘test gorilla’ site image when Veilus is loaded up. I do that to make sure the script got to the right place. But I probably want this checked every time a test goes to that page. And I don’t want to have to repeat that expectation or assertion each and every time.

Tapestry provides a Ready module. This allows you to put a page_ready declaration on definitions. Modify your Veilus class to look like this:

What that’s doing is providing a “ready validation” that allows you to specify what “ready” means for this definition. In this case, I’m saying “ready” means that the element that I’ve called test_gorilla is found to exist on the page. But you could wait for any condition that returns a boolean value. For example, you could wait for an element to not be present, such as a spinner indicating data is loading or something along those lines.

You can also specify a message to appear if the validation is found to fail. For example, you could change the above declaration to this:

The text will be the error message that is returned to explain the problem. Finally, you can check for multiple ready conditions. For example, you could have the following:

Each ready validation will be checked in the order they are specified in the source.

But there are some complications here so let’s talk about that.

Ready Validations

The begin_with worked as it did because it was essentially providing a sort of initialization that you want to happen. Perhaps you would load up some data here or you would do as I did and get the browser into a state you wanted.

The page_ready, however, can contain assertions or expectations about the page itself, which means the page needs to render. That means the page_ready declaration can’t just be executed the moment the page definition is instantiated. So how do you check it? Tapestry exposes two methods you can check: ready? and ready_error. For example, you could do this:

The ready? method will execute all ready validations on the page definition and return a boolean value. In the event of a validation failure, a validation error can be accessed via the ready_error method on the page. Here’s another example:

Here maybe you don’t want to stop up the test if the test gorilla image is missing. Perhaps you’ll want to note that, but not stop the test because of it. But, on the other hand, you do want to stop the test if the open login element was missing because, quite obviously, you won’t be able to log in anyway. So you might do this:

So here if the test gorilla was not present, this could be checked for but would not cause an exception, while the missing open element would. Do note that, as mentioned, the ready validations are checked in order of how they are entered on the definition. While there is no ordering mandated, you will want to put the ones that should cause a stop (like the example above) first in the list.

Another way you could use the ready validations is by calling when_ready on the definition instance and then providing a block of what you want to have happen if the page is, in fact, ready. Here’s an example:

Here you will view the page as you normally would but then the actions for logging in are now wrapped within the “when_ready” call. Since this might be something you want to do as part of viewing anyway, you can call this same block on the view call. Here’s what that would look like:

If you’re really into compressing your logic you can just do all of this when you instantiate the definition:

With any of these approaches, you can take out the following line from the test script:

Here’s the Full Logic

Lots of discussion and changes here so let me show you my final script:

Actually, there is one more change you can make. You can take out line 7, which is this line:

We’ve been carrying this line along with us but that was because, originally, we were including Tapestry in the top level namespace so we could use Tapestry directly at that level. But now we’re to the point where we are doing what we should do: delegating to an organizing module, the Veilus class in our case. And remember that, as you seen on line 10, we are including Tapestry in that module.

Delegate To Definitions

I hope what you’ve seen here is that while Tapestry supports the very standard approach of using a definition as a page object, it provides a few extras that go a little beyond the standard concepts. And yet, really, it’s not that far beyond.

In fact, all this is showing is a micro-framework approach, wherein you delegate to a repository (definition, page object) but have the micro-framework call into those definitions to provide functionality. However, the micro-framework doesn’t fail to work if those aspects aren’t present. This isn’t really a “communication pattern” in any recognized sense of that term, but I think it should be.

If you think back to the first post, we started out with no page definitions and no element definitions, but we still had working scripts. The framework provides hooks that it will look for. And if those hooks are present, it will modify its behavior accordingly.

Anything Left?

I think I’ve got one more immediate post in me on this topic. In the next post, I’ll show that Tapestry supports the standard page object pattern as you would hopefully expect. But I’ll introduce a little twist on it that I think is important for keeping tests concise and expressive.


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.