Heads up! This post originated on Medium and might be best read there. Click here to go to Medium.
Over the past year, my friend Wil and I have been building an acceptance testing library for single page apps called BigTest (thanks Frontside for sponsoring time!) We strongly feel the highest value tests you can write are ones that run in different browsers and test the whole application together.
While building out BigTest Wil wrote a library called interactor (@bigtest/interactor). You can think about interactors as composable page objects that are super fast. They wait for the element to be present before interacting, so you don’t have to put any sleeps in or sync up with any run loops. It also has a super-expressive API that makes writing complex tests more readable and maintainable.
The best part about interactors is they are composable so you can build on top of other interactors. Another great thing is you can use them anywhere there’s DOM. They were specifically built to be used with any framework.
Since interactors can work anywhere there’s DOM, they pair perfectly with Jest + jsdom component testing. With the mount helper from @bigtest/react, interactors can replace enzyme or react-testing-library.
Rather than talking about it, let’s just show it!
For this example, I wanted to keep it simple so we can focus on the testing API. When I wrote these tests I forked the ant-design component library and picked the Checkbox component as my test subject. But really any checkbox React component should work with these tests.
I picked the ant-design component library because it’s well maintained and already has a lot of Jest tests written. This allowed me to drop right in and use @bigtest/interactor. These tests are currently using enzyme to test the components. Let’s write a test for focusing the component to make sure it calls an onFocus callback when focus is set on the element.
First, we’re going to create our interactor. This will describe all of the different ways we can interact with a component. Things like clicking, scrolling, dragging, filling in inputs, focusing, etc.
We import two different methods from @bigtest/interactor:
Using these two methods we can focus the component, assert the onFocus callback works, and assert that the element is actually focused in the DOM. Now we can write our test!
We have to import the interactor we just created and then initialize it at the top of the test file. You can initialize an interactor to be scoped to a specific element on the page by passing a selector when calling new. We scope this checkbox interactor to a label element since the outer wrapper of the checkbox is a <label>. If you don’t want to scope it to an element, don’t pass anything. That will scope the interactor to the pages body.
Once it’s initialized we can start using it in our test:
Pretty neat! We mount the component in the DOM (jsdom, specifically) and then interact with it there. Let’s write a few more tests like checking for disabled state, toggling the checkbox, and more.
And then this is what the interactor looks like to back up those tests:
This is the tip of the iceberg for what you can do with interactors. You can create custom interactions on your interactor class, which make it so you can extend Interactors to fit the needs of your components. Check out the interactor guides and API docs to learn more!
I’ve mentioned a couple of times throughout this blog that interactors are composable. What does that mean exactly? Imagine your entire component library is tested with Jest and BigTest interactors. Let’s assume in that component library there’s a <Modal> and a <ConfirmationModal> component. When <ConfirmationModal> was built, it was built on top of the <Modal> component, which already has a tests & component interactor.
Both of these modals share similar actions but differ slightly. We can extend ModalInteractor and add what’s different for ConfirmationModalInteractor:
Just like when we composed the <Modal> component to make the <ConfirmationModal> component, we composed the ModalInteractor with ConfirmationModalInteractor to make writing tests easier.
It doesn’t stop there! When building the <Modal> component you probably used a <Button> component that takes all kinds of props. Things like disabling, handling actions, changing its style based on primary or secondary, etc. That button also has an interactor so you can add it to your ModalInteractor:
Now in the tests you have access to all of the ButtonInteractors properties through confimrationButton & cancelButton:
Modals can have all kinds of different components inside like buttons, checkboxes, input fields, etc. All of those components have their own tests and interactors for you to use! You can see how this would be really useful as you build more components and write more tests.
If you would like to see the ant-design example here’s a link to the commit on my fork. If a component library like ant-design were to use interactors in their tests, they could ship the interactors with the library. Then the developers who consume the library can use the interactors in their tests. 🔥
It doesn’t stop there either. Now that you have an entire component library tested with interactors, you can write application acceptance tests with ease. With BigTest we can import those interactors from our component library and use them in our acceptance tests.
Let’s imagine we’re writing an acceptance test for our apps sign up form. This form has two text inputs for a name, a checkbox for email subscribe, and a submit button.
On this sign up page we fill in the first name field, the last name field, check the email subscribe checkbox, and click the submit button. All of this is done through-composed interactors:
Your interactors for pages will compose a lot of different components. Writing tests now is a whole lot easier because you’ve already figured out how to move your components around. In the acceptance test example above, we didn’t write any custom interactions specific to the page to write the test. All of the interactions were expressed through nested interactors supplied from the component library.
Using @bigtest/interactor with Jest is a power combo for making component testing painless. On top of that, the BigTest framework makes writing acceptance tests for single page apps easier by utilizing interactors from the component tests.
I think this can be very powerful for design systems to leverage. Since BigTest is UI framework and test framework agnostic anyone can consume the interactors to start testing their components. Large companies who have a design system that spans the organization and ship in different frameworks (like Vue, React, & Angular) can leverage interactors and each team can still import those interactors to use in their tests. One interactor for three different UI frameworks. That is super powerful, imo.
I feel like I have to put a small disclaimer at the bottom of this post stating that while a lot of people love jsdom, I’m not so much of a fan. It’s an implementation of the DOM that isn’t used by any actual browsers. BigTest was created specifically to not do that and run tests across many real browsers.
That’s all to say I think you should cross-browser test your app at some point. If that’s done by using Jest + jsom + interactors for components and BigTest for acceptance tests then that warms my heart ❤️😎
Lastly, shout out to Ryan Rauh for opening my eyes to using Jest with BigTest interactors! 💯