In these weeks, I always want to learn about writing tests using Jest, Mocha or other stuffs. Unfortunately, I don't have any React apps that want to test these days but I have chances to figure out how to test HTML page with pure JavaScript.
I found most tutorials are using Jest with React or other JavaScript frameworks. Is it really possible to test HTML page with Jest? Yes!
How to start
View the demo here for my sample HTML page. Just a simple page to fetch JSON, show a list based on it and a button to show/hide translations.
In your root folder, create a package.json
like this, and run npm install
in console.
{
"scripts": {
"test": "jest --watch",
"coverage": "jest --coverage"
},
"devDependencies": {
"jest": "^23.6.0"
}
}
When you finished, you can start testing your app! Create a file <YOUR-FILENAME>.spec.js
and start testing like:
const fs = require('fs');
const path = require('path');
const html = fs.readFileSync(path.resolve(__dirname, '../index.html'), 'utf8');
jest
.dontMock('fs');
describe('button', function () {
beforeEach(() => {
document.documentElement.innerHTML = html.toString();
});
afterEach(() => {
// restore the original func after test
jest.resetModules();
});
it('button exists', function () {
expect(document.getElementById('disable')).toBeTruthy();
});
});
Save it and run npm test
!
What I learned
1. Difference between testing HTML and React page
In React, you can use Enzyme to shallow
the component and get state for testing. But in HTML and JavaScript page, the only thing you can test is the classes, content and function output. When you are used to test by states, it might be not so convenient to test HTML page.
As pure JS did not export anything for Jest to test, you also need to add this at the end:
if (typeof exports !== 'undefined') {
module.exports = {
getItem,
setItems,
triggerItem
};
}
Then Jest can import/export functions for testing.
For HTML, you cannot directly import it like React component. You need to add this snippet before tests, to import the whole HTML:
const fs = require('fs');
const path = require('path');
const html = fs.readFileSync(path.resolve(__dirname, '../index.html'), 'utf8');
jest
.dontMock('fs');
or write inline HTML inside tests. For example:
const html = document.createElement('div');
html.innerHTML = `<div class="lang-grid" id="language">This is a sample</div>`;
document.body.appendChild(div);
2. Special way to test asynchronous code
It is still easy to test basic thing, like checking the menu button will appear in mobile view using Jest. Like:
it('menu button exists', function () {
expect(document.getElementById('menu-btn')).toBeTruthy();
});
But for asynchronous code, like Promise, you need to use different approach.
The most important thing is to add done()
in each test.
it('get same items from json', function (done) {
fetch.mockResponse(JSON.stringify(json))
const {getItem} = require('../scripts/main.js');
getItem().then(res => {
expect(res).toEqual([{
"phase": "Entschuldigung!",
"trans": "Excuse me. [as in may I have your attention]."
},
{
"phase": "Sprechen Sie Englisch?",
"trans": "Do you speak English?"
}])
expect(res.length).toEqual(2);
done();
})
.catch(err => console.log(err))
});
Just like what Jest documentation said, it is important to add done()
in the test. Otherwise, it may have wrong results.
After you added done()
, it will wait till your async call to be resolved and get the expected result.
3. Check coverage using Jest
Jest has built-in coverage function, you can call it using jest --coverage
. Then you can see your reports in coverage/lcov-report/index.html
. The chart is very useful and inform you which code have not tested.
(Why it is not 100% in branches? Because I skipped the test of exporting module statement at the end. )
I have not use coverage tool before, that's why I am motivated when I saw my code changed from red to green!
Is testing fun?
Well, it may not be fun, but surely is satisfactory when I saw my code changed from RED to GREEN.
Do you have any suggestions to my flow? Or any ideas on testing? Feel free to drop me a line here :)
Top comments (10)
Try to avoid testing implementation details, it’s better to focus on testing functionality.
In you case I would test result in UI, rather than “get item” function. In that case, you can make refactoring, change name of the function, transfer to different module, and tests will help you to validate that you didn’t break functionality of the code.
Then you testing implantation, you need to change your tests with your code, so it’s not very helpful.
Thanks for your reply!
I am still a beginner of testing and I found that it is hard to learn how to write good tests. You reply is extremely helpful. Thanks! :)
Great article!
I am curious about one thing, how did you deal with all the unsupported features from jsdom such as MutationObserver, or the inability to access input fields by name without using
querySelector
likeformName.inputName.value
. Did you have to manually install polyfills for each or maybe you did something else?Wait a minute.. is
document
exposed by jest by default? Or how that works? 🤯It is exposed if you set
testEnvironment: 'jsdom'
as an option.
Thanks Yuki for writing this! I had a similar question of how to test an HTML file with vanilla JavaScript and no UI frameworks, and your article gave me a really good starting point.
I ended up finding a way to do this with Jest and Kent Dodd's DOM Testing Library, and I wrote about it here: dev.to/thawkin3/how-to-unit-test-h...
Yes, testing library is quite good and it forced you not to use shallow or create lots of mocks for different functions.
I am using react-testing-library currently and it is fantastic! It is great to learn how to write unit tests using testing library and learn those best practices :)
Many thanks for this article, I was struggling in so many point you highlight :D
You make my week :D
Super helpful Yuki!
Thanks :)