Chapter 3.1.2: Basic test setup with Cypress and Vitest: Cypress watch mode and our first two tests
We are configuring Cypress to run tests after automatically changing our application's code, localizing Cypress helper functions, and writing our first two tests.
Disclaimer: You are reading an early version of the text! The final version of the book will be revised and may contain additional content.
Cypress watch mode
Many years ago, close to the beginning of my career as a web developer, a new era began: Suddenly, we had more and more sophisticated tools that watched for modifications to files and automatically reloaded the browser whenever they detected a change. I can still remember the relief it was for my daily workflow when I first discovered one of these tools. Today, a watch mode is standard functionality. Thanks to sophisticated build tools like webpack and Vite, live reloading, or even Hot Module Replacement (HMR) became a piece of cake.
But a watch mode also makes our work much more comfortable when writing tests. Since Jest at the latest, a corresponding functionality has also become an indispensable feature for unit test frameworks. Our unit test tool of choice, Vitest, offers an even more sophisticated watch mode than other such tools. Unfortunately, Cypress does not include a watch mode out of the box. But we don't want to do without this comfort feature when writing application tests. Luckily there is a plugin for it. With cypress-watch-and-reload
, we can easily retrofit this functionality.
npm install --save-dev cypress-watch-and-reload
After installing the cypress-watch-and-reload
npm package, we still need to activate it in our Cypress setup.
// cypress/plugins/index.js
import watchAndReload from 'cypress-watch-and-reload/plugins';
export default (on, config) => {
watchAndReload({
'cypress-watch-and-reload': {
watch: [`src/**`],
},
...config,
});
return config;
};
// cypress/support/index.js
// ...
import 'cypress-watch-and-reload/support';
// ...
After we have everything set up, Cypress will monitor every change to our code and run the tests automatically if necessary. However, since we probably don't want to run all the tests every time we change something in our application, we should mainly use the watch mode with the only
attribute. This way, we can ensure that Cypress only runs those tests directly related to the code we are working on.
// Only this test is executed.
it.only('should not ...', () => {
// ...
});
// This one is skipped.
it('should show ...', () => {
// ...
});
Live reloading makes TDD truly enjoyable! After all, we can save a lot of time because we don't have to manually click and type to test if our code is working correctly. Vitest starts automatically in watch mode. The cypress-watch-and-reload
package gives us the same convenience when working with Cypress.
Local Cypress types
I don't know exactly why, but it is conspicuous that many testing frameworks inject helper functions like test
, description
, and expect
globally. I suspect that one framework started doing this at some point, and all subsequent frameworks simply adopted this practice because users were already used to it. Of course, I am aware that it is very comfortable not to have to import numerous helper functions in every file. But at the same time, I don't see it as a big problem either. After all, virtually all IDEs offer a function that automatically inserts the required import statements.
// Cypress globally injects `it`, `cy`, and `expect` by default.
it('should work as expected', () => {
cy.visit('/');
expect(1).to.equal(1);
});
From my point of view, we should avoid global functions at all costs. We don't gain much from them, but we have to deal with the fact that tools like linters or the type hinting of our code editor have to be explicitly configured to handle global variables and recognize them correctly. Vitest has done the right thing, in my opinion, and does not provide helper functions globally by default. Instead, we have to import them manually. Unfortunately, this is not the case with Cypress, and it is not something that we can easily change via the configuration. Fortunately, once again, a plugin makes it very easy for us to customize our Cypress setup according to our preferences.
npm install --save-dev local-cypress
After installing the local-cypress
dependency, there is nothing more to do than import Cypress helper functions from the local-cypress
package instead of relying on Cypress injecting them globally.
import { cy, expect, it } from 'local-cypress';
it('should work as expected', () => {
cy.visit('/');
expect(1).to.equal(1);
});
The local-cypress
plugin by Gleb Bahmutov makes it possible to import the Cypress helper functions only when needed. Importing the helper functions makes their origin fully transparent. Not only for us developers reading the code but also for tools like linters and our IDE, which can thus handle them without us having to configure these tools specifically for Cypress.
Our first two tests
The basic setup is ready. Now it's time to write our first barebones tests to see if everything works the way we expect. Consequently, we will write a unit test and an application test. Unit tests always refer to a specific function, class, or Vue component. Hence, it makes sense that we also store the test file directly in the same place as the code we are testing.
// src/components/HelloWorld.spec.ts
import { expect, it } from 'vitest';
import { mount } from '@vue/test-utils';
import HelloWorld from './HelloWorld.vue';
it(`should render the given msg as the primary headline`, () => {
let wrapper = mount(HelloWorld, { propsData: { msg: `Hello World` } });
expect(wrapper.find(`h1`).text()).toBe(`Hello World`);
});
In this very primitive test for the HelloWorld
component, which the default Vue.js setup of Vite gives us, we only test if the component renders the passed message correctly. In daily practice, we will, of course, write tests that check more complex logic. But for now, the test serves its purpose.
Tests that we use to test entire features of our application, we call application tests. Since a given feature may consist of many components or even several pages, storing the corresponding specification files directly with the source code makes less sense. There is no 1:1 relationship between a test and a particular file in our project. Therefore, we create a file in the ./tests/specs
directory for our first application test.
// tests/specs/hello-vue.spec.ts
import { cy, expect, it } from 'local-cypress';
it(`it should greet Vue`, () => {
cy.visit(`/`);
cy.get(`h1`).should(($headline) => {
expect($headline.text()).to.include(`Hello Vue`);
});
});
The demo app Vite created for us does not contain any complex logic. Accordingly, this test is also very simple and only tests that a particular message is displayed correctly on the home page. Of course, this is also because we do not yet have a real application to test. As a first step, however, this test will do.
Now everything is ready, and the moment of truth is approaching. Last but not least, we add some scripts to our package.json
file to run the tests.
{
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "vitest run --config ./tests/driver/vitest/vitest.config.ts",
"test:unit:watch": "vitest --config ./tests/driver/vitest/vitest.config.ts",
"test:unit:coverage": "vitest run --coverage --config ./tests/driver/vitest/vitest.config.ts",
"test:application": "cypress run --config-file ./tests/driver/cypress/cypress.config.json",
"test:application:watch": "cypress open --config-file ./tests/driver/cypress/cypress.config.json"
}
}
With the command npm run test:unit
, we can run our unit tests with Vitest. We see that our first test runs blazing fast. After that, we can also start npm run test:application
to run the application tests in Cypress. Especially on the first startup, it takes a lot longer to run this test. This is because Cypress runs the tests in a real browser. Running our tests in a real browser has many advantages, like ensuring that our app runs under real-world conditions. But this comes at the expense of being significantly slower.
Now we have Vitest and Cypress installed in our project and set up so that we can rely on them for subsequent improvements. We configured the base setup so that working with it is very convenient. Convenience and ease of use are critical because we don't want to think much about our test setup in the future. It should be a tool that just works and allows us to write tests quickly using TDD. The TDD process will show us how to implement features better. Convenience features like a watch mode are essential for this, in my opinion.
Things are getting very interesting and it ties in well for me personally with the previous chapters explaining the Whys, Wheres, Whens, etc. The exactly How it can be done becomes much more valuable with knowing why we do things the way they are done. Thanks I'm finding solid value and a much clearer picture of TDD!