The Architecture of a Micro-Framework

In a series of posts, I’ve talked about my Tapestry micro-framework and I tried to provide some of the rationale for its design choices. Providing that rationale meant providing a context for you to see it in action. This post will cap off the previous posts by digging into the code of Tapestry a bit and showing you how it works. I hope this is more relevant given that you’ve now seen it in action.

I’m most certainly not going to cover every line of code here. Rather my intention is that of a very brief but broad-lens view, showing you a bit of the moving parts and the connective tissue between them.

There’s Automation and There’s Test Tooling

A lot of people conflate the act of writing automation with the act of writing test tooling that allows for automation. Both are acts of development. Development utilizes both design and programming as techniques to get something out the door. I bring this up because Tapestry provides a way to conduct automation, but it is not an “automation tool” necessarily. It’s a tool that allows automation to take place.

The reason I bring this up is because since this is development, the practices we hope developers follow are those we should follow.

For example, Tapestry does have unit tests. Tapestry development is checked for style and design issues (“linting”) via a hound file that is read in by RuboCop. Tapestry does provide some basic examples to give an idea of how automation looks when it’s written with Tapestry. And, finally, Tapestry has a relatively minimal footprint in terms of the moving parts (i.e., files that make it all work).

Clearly Tapestry is versioned and stored in a repository. It is also part of a continuous integration model, currently using Travis, which is running my unit tests each time as well as my RuboCop code style checks.

But even more importantly, a key thing to realize is that my automation and my test tooling are separate. What that means is someone writing automation with Tapestry could version, store, and deploy that automation separately from Tapestry and, in fact, should do so. That’s a key point of what makes a framework or micro-framework: the ability for it evolve and be separate from that which uses the framework.

Tapestry Wants to Be Included

The main Tapestry file is called, not surprisingly, tapestry. If you think back to how I showed the page definitions working, you include Tapestry as part of them like this:

That works because of the included method. What this means is that Tapestry is essentially mixed-in to any definition. And a definition is just a class, of course. So here the conceptual basis of Tapestry (definitions) merges well with the practical implementation of such (classes).

Being included provides a lot of things to the class but it also opens up a way for Tapestry to look into the class and act accordingly if it finds something that it knows how to deal with. You’ve seen this through the various posts in this series and I’ll reacquaint you with a few of those aspects here.

Tapestry Wants To Provide Certain Things

In Ruby, when a module is included and the class that it’s included in is instantiated, then Tapestry’s initialize method is also executed. That method is setting the @browser instance variable, which is pretty critical for making sure that Tapestry can reference whatever browser is being run against.

But how is that browser being set up? If you remember, we had statements like this in our test logic:

Those are methods being called directly on Tapestry itself and are part of the class.self implementation. I don’t necessarily want to turn this into a Ruby tutorial but check out Metaprogramming in Ruby: It’s All About the Self if you are curious.

If you remember, I was also doing things like this in the test logic:

Those are coming from the interface module (specifically move_to and maximize). The reason this works is because of that “included” call I referenced earlier. Specifically, the Interface::Page module is included as part of Tapestry, just as Tapestry is included as part of the definition.

What this means is you could have a “Interface::Screen” module for handling, say, screen objects for mobile testing. Or you might want an “Interface::Service” module for handling API calls. Or maybe even an Interface::Contract for contract-based test support.

Tapestry Likes Attributes

From the previous posts, you might remember that you can put assertions or declarations on the page definitions:

Behind the scenes, Tapestry considers these to be attributes that are asserted for (or declared on) the definition. These are provided by the attribute module. This is handled a little differently than the inclusion I mentioned before. The attribute module is extended to whatever included Tapestry. This means that anything that included Tapestry can call these methods on itself, as opposed to on Tapestry.

Once it’s included, think of Tapestry as extending parts of itself to make those parts available to the definition as if they were defined as part of the definition. Note that all of this is being done without any sort of complex inheritance chain.

Tapestry Has Definitions Within Definitions

In some of the previous posts, we included element definitions within the page definitions, like this:

This is all handled by the element module (which, like attributes, are extended into whatever included Tapestry). What allows this to be possible is two things:

There are plenty of comments in that module that should give you some idea of what’s happening.

I should note as well that the Element module does include an internal module called Locator. And there is an access_element method on that Locator module which is what allows the element definitions to be treated as a locator by the browser library and then sent to the browser driver.

This All Evolved … And is Evolving

This architecture evolved as I learned more about how I wanted the test logic to be expressed. In fact, Tapestry evolved even as I was writing these posts, as you might have noticed when I called out the need to update to specific versions in a few posts.

Part of the strength of this, at least as I see it, is that Tapestry only does what it does and tries to do it fairly well. It relies on extensions for doing things better or differently. For example, Tapestry has some built in extensions. Others can be incorporated as separate Ruby projects, such as support for test workflows.

And, with that, this should close off this series of posts for now.

Future of Tapestry?

Tapestry is something I plan to continue to support for some time to come. I would like to port this to other languages, like Python and JavaScript, ideally keeping as much of the interface as possible. Obviously those languages will have different behind the scenes implementations, but I would love it if I could have each such implementation support the context factory approach, such as this:

Just like that. With no boiler-plate code surrounding it. With no unnecessary “self” statements or “static” calls or anything.

One of the reasons I’ve chosen Ruby so much and tend to promote it as a test solution development language is because it allows you construct very minimal logic like the above and provide that via framework logic that is relatively simple to construct.

I will do some more posts on Tapestry in the future, including how some of my other provided gems work with it. But I hope this series of posts, taken collectively, give you some insight into how at least one test solution developer approached the ideas and implemented them. Tapestry is open source, so feedback and pull requests are always welcome.


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.