migrating to Sapper part 2 - TDD with Cypress.io
Today, we will look into how I migrated from the previous GatsbyJS/React blog stack to the new Sapper/Svelte one. It will be the perfect time for me to talk about Test Driven Development (TDD).
This article is part of a series of posts about migrating from GatsbyJS/React to Sapper/Svelte. You can check the other posts: part 1, part 3, part 2 bis, and more to come!
Test Driven Development
So, what is TDD?
It is the process of writing tests first, before your code.
- this test will fail (RED)
- write only the code needed to make it succeed (GREEN)
- then look at all the tests and the all the code, and refactor.
Of course, refactoring does not introduce new features, it is merely moving things around, renaming variables, methods, functions, classes to make them more readable, etc.
Red-Green-Refactor: Itβs like a mini-quest with a small mission. It is addictive in its nature.
Also, it is a discipline that forces you to not write unneeded code. And if you think about testing first, your code will be tested and designed to be tested.
It is a discipline and a practice, I recommend you to find a mentor to learn it faster.
If you want some solid (SOLID ) book ideas, go read Test Driven Development: By Example by Kent Beck.
β¦And Iβm not a real master, merely trying to follow this discipline. Next, weβll see how I tried to.
non-regression testing
My aim with non-regression tests would be to ensure the new blog would be as good as the old one, feature-wise.
Well, it is not really TDD, it is a little bit different. You see, I had not written tests first because I used a template for the first blog. My tests were not automated, they were me checking how it works by opening the browser and clicking and looking around.
For the next iteration of the blog, I would need to do this manual testing and I thought that I could automate things to shave a few hours of manually verifying features.
So the process was:
- I have an existing website. I suppose it is 100% working.
- I create a test expressing its features after the fact (non-regression or retro-testing)
- I iterate until this test is green on the existing website.
- then I run the exact same test on the new website, it turns red.
- I then iterate by adding code to make it pass (green).
- I refactor the tests and the code.
So as you see, the last part is test-driven. The first part is the exact opposite. But the whole process enabled me to quickly reproduce the existing features I wrote as tests into the new website.
Writing tests with Cypress
As youβve seen, for each of my previous blog features, I added a Cypress.io test!
If you donβt know Cypress.io, go check it.
Itβs an awesome end-to-end (a.k.a. βE2Eβ) testing solution.
The user experience is really great, and the tool is fast!
Writing tests is also really simple compared to other E2E tools.
NOTE: I am not affiliated with them in any way.
What came before:
WebDriver or Selenium (they are both more or less the same thing), Appium (WebDriver based for mobile), Protractor(Angular tooling over Appium over WebDriver.. you get it): you have failed to be reliable and simple to me.
Why? The moment you see a tool abstracting a tool abstracting a tool abstracting user interaction, you know you will have a hard time identifying issues when something breaks in one of those layersβ¦even more, when they are asynchronous.
What Cypress fixes
Now Cypress tests are:
- fast to execute,
- simple to write,
- easy to debug with good developer tooling,
- quick to add to you Continuous integration pipeline,
- and reliable.
So I wrote Cypress E2E tests as a non-regression layer, expressing βbusinessβ rules about blog features that would be green on the older blog, and red on the new one.
You can check them on the GitHub repository in /cypress/integration
.
Here is a sample log of the output (edited to shorten it):
npm run cy:run
> cypress run
====================================================================================================
(Run Starting)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Cypress: 3.3.1 β
β Browser: Electron 61 (headless) β
β Specs: 6 found (404.js, article-footer.js, blog-post.js, footer.js, home.js, privacy-polβ¦ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Running: 404.js... (1 of 6)
The 404 page
β should show up for unknown URLs (2586ms)
β should show have a TUMBLR TARDIS lost in space GIF (152ms)
β should have the "built with" footer (95ms)
β should have the "David's Blog" Title Bar (306ms)
β should have a back link to home (216ms)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Running: article-footer.js... (2 of 6)
The "Article Footer"
β has David's name in it (740ms)
β has a link to Senlis Wikipedia page (187ms)
β has a link to David's Twitter profile page (188ms)
β has a link to David's GitHub profile page (181ms)
β has a link to David's Codepen profile page (147ms)
β has a link to David's DEV profile page (216ms)
β has a link to David's LinkedIn profile page (276ms)
β has a link to StackOverflow profile page (210ms)
β has a link to David's GitLab profile page (204ms)
β has a link to David's Facebook profile page (154ms)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Running: blog-post.js... (3 of 6)
a blog post
β should show the title bar in h3 (2123ms)
β should have a back link to blog post list/home (333ms)
β should show the published date in the first <p> following the <h1> article title (309ms)
β should show the time to read in a second <p> following the <h1> article title (349ms)
β should show the "written by" footer (293ms)
β should show the "built with" footer (254ms)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Running: footer.js... (4 of 6)
has a footer
β shows a copyright year (650ms)
β shows a link to Svelte (272ms)
β shows a link to Sapper (149ms)
β shows a link to Privacy Policy (162ms)
β can link to Privacy Policy (266ms)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Running: home.js... (5 of 6)
David's Blog app
β has the correct <h1> (655ms)
β show blog posts list as home with at least 4 posts (287ms)
β shows the Article Footer (189ms)
β shows the built with footer (203ms)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Running: privacy-policy.js... (6 of 6)
Privacy Policy page
β should have the proper title (732ms)
β should have the cookie monster gif (167ms)
β should have a maitlo link (154ms)
β should have a back link (177ms)
β should show the Article Footer (556ms)
β should show Built-by footer (801ms)
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β 404.js 00:03 5 5 - - - β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β article-footer.js 00:02 10 10 - - - β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β blog-post.js 00:03 6 6 - - - β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β footer.js 00:01 5 5 - - - β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β home.js 00:01 4 4 - - - β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β privacy-policy.js 00:02 6 6 - - - β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
All specs passed! 00:15 36 36 - - -
As you see the tests are like an executable specification of the expected features of my blog.
And this specification can be checked by executing the tests.
Sample test
I will not go too far into details, I urge you to go check Cypress.io Get Started and Writing your first test.
There will be a sample test below, though what I will share is something I found useful during my process of targeting one dev server and the other.
Targeting old or new website
There is one thing that I will detail, it is using an environment var so that I could quickly target either the old blog or the new one.
I used the following Cypress config file:
cypress.json
file:
{
"baseUrl": "http://localhost:3000",
"video": false,
"env": {
"BLOG_JAMSTACK": "sapper"
}
}
Which can later be used in JavaScript files to reference environment variables with (sample from cypress/common.js
):
const configuredJamStack = Cypress.env('BLOG_JAMSTACK');
So the value will be the one from the BLOG_JAMSTACK
environment variable. As Cypress picks up the cypress.json
file above, its default value will be sapper
.
On the terminal, I created and ran two different NPM Run Scripts, defined in package.json
:
"scripts": {
...
"cy:open": "cypress open",
"cy:open-legacy": "cypress open --config baseUrl=http://localhost:8000 --env BLOG_JAMSTACK=gatsby",
...
}
The first one npm run cy:open
, would use the cypress.json
file above and thus target Sapper (new blog).
The second one npm run cy:open-legacy
would target the Gatsby version (old blog) that can even run concurrently on another port (dev mode).
In tests I can then use variables that depend on the target website, like this:
import { mainFrameworkName, mainFrameworkURL } from '../common';
describe('has a footer', () => { // main section = website component or feature
beforeEach(() => {
cy.visit('/') // tells Cypress to go to the root URL each time it starts a test in this describe section.
});
it('shows a copyright year', () => {
cy.get('footer') // we select the <footer> tag
.should('contain', new Date().getFullYear()) // the current year (dynamic)
});
it('shows a link to ' + mainFrameworkName, () => {
cy.get('footer')
.get(`a[href*='${mainFrameworkURL}']`) // here we use the mainFrameworkURL from common.js which depends on target website (Gatsby or Sapper urls)
.should('contain', mainFrameworkName)
});
it('can link to Privacy Policy', () => {
cy.get('footer')
.get(`a[href*='privacy-policy']`)
.click() // we also check navigation by clicking here to see if some links does indeed redirect to an expected URL.
cy.location('pathname').should('contain', '/privacy-policy')
cy.get('h1').should('contain', 'Privacy Policy')
});
});
Go check in the repository the full code for more details or examples.
The tooling
Here are what you can see when you run the cypress open commands
, which will help you see and pinpoint what happened and how to improve or write selectors and test them inside the context of the browser.
You start with all your tests in a list:
You can run a test collection and see them unfold to the right pane:
You can check each instructionsβ effects on the browser, going back each step to visually see what happened:
Using it to test CSS selectors for your cy.get('...')
instructions:
Conclusion
In this second installment about how I migrated my previous blog from GatsbyJS/React to Sapper/Svelte, we have talked about TDD, non-regression testing, End to End tests and Cypress.io
It was less funky than my previous post, lacking emojis, animated GIFs and pictures of from my kids, but I hope it was still enjoyable!
If you have any questions about TDD, Cypress, Svelte, Sapper, or just want to say hi or thank you, my DMs are open on Twitter!
Photo credit from Unsplash by @winstonchen, @cbarbalis, @randytarampi.