Collection of unit test best practices #814
Replies: 8 comments 8 replies
-
Bad: Loading external resources via
|
Beta Was this translation helpful? Give feedback.
-
Bad: Loading external scriptsLike loading external resources via fetch / XMLHttpRequest, this is bad for several reasons. Refer to the post above to get more insight. Usually Bad// blocks/instagram/instagram.js
import { createIntersectionObserver, createTag, loadScript, isInTextNode } from '../../utils/utils.js';
export default function init(a) {
if (isInTextNode(a)) return;
const embedInstagram = async () => {
const anchor = createTag('a', { href: a.href });
const blockquote = createTag('blockquote', { class: 'instagram-media', 'data-instgrm-captioned': '' }, anchor);
const wrapper = createTag('div', { class: 'embed-instagram' }, blockquote);
a.parentElement.replaceChild(wrapper, a);
loadScript('https://www.instagram.com/embed.js');
};
createIntersectionObserver({ el: a, callback: embedInstagram });
}
// instagram.test.js
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import { waitForElement } from '../../helpers/waitfor.js';
const { default: init } = await import('../../../libs/blocks/instagram/instagram.js');
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
describe('instagram', () => {
it('renders embed post for instagram link', async () => {
const instagram = document.querySelector('a');
init(instagram);
// the await is needed as https://www.instagram.com/embed.js is being loaded
// if the script times out, the test will fail
const iframe = await waitForElement('iframe');
const blockquote = document.querySelector('.instagram-media');
const wrapper = document.querySelector('.embed-instagram');
expect(blockquote).to.not.be.null;
expect(wrapper).to.not.be.null;
expect(iframe).to.be.exist;
});
}); GoodWeb test runner allows using import maps with import maps we can mock the milo utils and ensure that // embed-utils.js
// when mocking ensure all the functions that are used are mocked.
// ....
import { stub } from 'sinon';
export function isInTextNode() {
return false;
}
export const loadStyle = stub();
export const loadScript = stub();
export function createIntersectionObserver({ el, callback /* , once = true, options = {} */ }) {
// fire immediately
callback(el, { target: el });
} // instagram.test.html
<html>
<head>
<script type="importmap">
{
"imports": {
// This ensure that another file is loaded instead of utils.js
// Depending on the nature of the script, you can also add other import maps
// Or other scripts that load the 3rd party script from localhost reliably.
"../../../libs/utils/utils.js": "./mocks/embed-utils.js"
}
}
</script>
</head>
<body>
<a href="https://www.instagram.com/p/Cl1xhZuvN8N/?utm_source=ig_web_button_share_sheet">https://www.instagram.com/p/Cl1xhZuvN8N/?utm_source=ig_web_button_share_sheet</a>
</body>
<script type="module">
import { runTests } from '@web/test-runner-mocha';
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import { stub } from 'sinon';
import { waitForElement } from '../../helpers/waitfor.js';
import { loadScript } from '../../../libs/utils/utils.js';
import init from "../../../libs/blocks/instagram/instagram.js"
runTests(() => {
describe('instagram', () => {
it('renders embed post for instagram link', () => {
const instagram = document.querySelector('a');
// We don't need to wait for the script to load
init(instagram);
const blockquote = document.querySelector('.instagram-media');
const wrapper = document.querySelector('.embed-instagram');
// We can also test that the script would have been loaded
// and assume instagram *would* have been rendered.
expect(loadScript.callCount).to.equal(1);
expect(blockquote).to.not.be.null;
expect(wrapper).to.not.be.null;
});
});
});
</script>
</html> Real world exampleHere is an example of a good test for Another example that migrated some tests that were loading external scripts: #701 |
Beta Was this translation helpful? Give feedback.
-
Good: Avoiding 404 error logs404s are leading to noise and are not necessary. Ensure that you either mock the resources, or don't load them if they aren't needed for a test. Bad // Bad mock data copied over from production
<p>
<picture>
<source type="image/webp" srcset="./media_109b289ba2841014b405e8cda9dbeb3e6f7471201.jpeg?width=2000&format=webply&optimize=medium" media="(min-width: 400px)">
<source type="image/webp" srcset="./media_109b289ba2841014b405e8cda9dbeb3e6f7471201.jpeg?width=750&format=webply&optimize=medium">
<source type="image/jpeg" srcset="./media_109b289ba2841014b405e8cda9dbeb3e6f7471201.jpeg?width=2000&format=jpeg&optimize=medium" media="(min-width: 400px)">
<img loading="eager" alt="" type="image/jpeg" src="./media_109b289ba2841014b405e8cda9dbeb3e6f7471201.jpeg?width=750&format=jpeg&optimize=medium" width="2000" height="875">
</picture>
</p> Good // Better mock data that doesn't load the resources.
<picture>
<source type="image/webp" srcset="" media="(min-width: 400px)">
<source type="image/webp" srcset="">
<source type="image/jpeg" srcset="" media="(min-width: 400px)">
<img loading="eager" alt="" type="image/jpeg" src="" width="2000" height="875">
</picture> |
Beta Was this translation helpful? Give feedback.
-
Bad: Building up test state from other testsTests that share state are heavily discouraged! They are very tedious to debug and can lead to unexpected behavior. Starting "fresh" and setting state in every Generally, if you put a Baddescribe('example of a bad test!!', () => {
// only setting a 'before' once
before(() => {
document.body.innerHTML = "Text"
})
it('test one modifies some state, either on the DOM or within a class, or anything shared.', () => {
expect(document.body.innerHTML).to. Equal("Text")
// modifying "shared" state
document.body.innerHTML = "Text2"
})
it('depends on the state of the previous test', () => {
// depend on the state another test (no before/beforeEach) set .. very bad!
expect(document.body.innerHTML).to.equal("Text2")
})
}) Correctdescribe('example of a good test', () => {
// beforeEach ensures the "shared" DOM state is always predictable in its base state before each test
beforeEach(() => {
document.body.innerHTML = "Text"
})
it('This test does not modify anything.', () => {
// At the end of this test, we know that the beforeEach will reset body.innerHTML.
expect(document.body.innerHTML).to.equal("Text")
document.body.innerHTML = "something else"
expect(document.body.innerHTML).to.equal("something else")
})
it('Does not depend on whatever test 1 set.', () => {
document.body.innerHTML = "Text2"
expect(document.body.innerHTML).to.equal("Text2")
})
}) Real World ExampleRefactoring the util tests to properly reset the head & body before each test using Tests were relying that the DOM had been modified in a specific way, by a previous test. As tests can run in parallel, when the DOM wasn't modified by a previous test - the test would sometimes fail leading to issues with sporadically failing tests on the CI. |
Beta Was this translation helpful? Give feedback.
-
@mokimo this is a great collection of best practices already. How granular did you want this discussion to get? I have had a few items I often will add during my Code Reviews that are a bit nit-picky, but I feel strongly about, for example: Ensuring describe('My block', () => {
it('adds text to a button', () => {})
it('modifies the structure based on metadata', () => {})
}) Where applicable, adhering to Testing Library's methodology of testing: using our expects to mimic what users would check as opposed to other checks (utilizing text, aria-attributes, etc). You show this good practice by checking the text in your example in "Building up test state..." above. // Bad
describe(`Toggle button`, ()=> {
it('adds the class to the previous toggle', () => {
toggle.click();
expect(toggle.classList.contains('open')).to.be.true;
})
})
// Good
describe(`Toggle button`, ()=> {
it('adds the class to the previous toggle', () => {
toggle.click();
expect(toggle.ariaExpanded).to.equal('true);
})
}) What do you think? |
Beta Was this translation helpful? Give feedback.
-
LOVE THIS. |
Beta Was this translation helpful? Give feedback.
-
We also have a few test utilites that can be useful:
If you find yourself creating a similar function, be sure to add it to the helpers dir. |
Beta Was this translation helpful? Give feedback.
-
This is quite valuable to everyone IMO, so I'm wondering whether a Unit Testing section could be added to https://milo.adobe.com/docs/engineering/ (or maybe https://github.com/adobecom/milo/wiki) with the principles outlined here. This might provide more visibility over time, as this thread will be pushed out of view. |
Beta Was this translation helpful? Give feedback.
-
We should collect best practices for writing web test runner unit tests and highlight how to fix bad patterns. This thread should serve a good entry point for anyone, to provide a blueprint on how to avoid certain patterns.
Beta Was this translation helpful? Give feedback.
All reactions