Not Another To-Do App: Part 3
Westbrook Johnson
Posted on August 19, 2019
Getting your hands dirty and feet wet with Open Web Component Recommendations...sort of.
This a cross-post of a Feb 26, 2019 article from Medium that takes advantage of my recent decision to use Grammarly in my writing (so, small edits have been made here and there), thanks for looking again if you saw it there đđ˝ââď¸ and if this is your first time reading, welcome!
Welcome to âNot Another To-Do Appâ, an overly lengthy review of making one of the smallest applications every developer ends up writing at some point or another. If youâre here to read up on a specific technique to writing apps or have made your way from a previous installation, then likely you are in the right place and should read on! If not, itâs possible you want to start from the beginning so you too can know all of our charactersâ backstories...
If youâve made it this far, why quit now?
Test Early, Test Often
For me, the best part of having a tool like Karma available in my application from the starting gun is that I can pretend that I know how to do TDD (test-driven development). Donât get me wrong, itâs not that I donât know what it means, or how to do it, my issue is more one of conviction. I love a round of ping pong pairing when getting into a session of pair coding. Itâs a great way to keep the time structured, and it can quickly bring to light any number of important conversations about the project/feature the two of you are entering on. Itâs after I get back to my own desk where things start to slip. Write one test, add one feature, write a second test, write a second feature... and, right around there it all falls apart. Itâs great when it works though, and in that ethos, I started my To-Do app as follows:
it('has a title', async () => {
const el = await fixture(html`<open-wc-app></open-wc-app>`);
const title = el.shadowRoot.querySelector('h1').textContent;
expect(title).to.equal('open-wc');
});
Ok, ok, you got me, I didnât start it that way. It came for free from our friends at open-wc. Test coverage before I type a key, yay! But, I did still start with a test and it looked something like:
const el = await fixture(html`<open-wc-app></open-wc-app>`);
expect(el.todos.length).to.equal(0);
Which of course fails (TDD, what?!). In true LitElement form the following gets the test back to green:
class OpenWcApp extends LitElement {
static get properties() {
return {
todos: { type: Array }
};
}
constructor() {
super();
this.todos = [];
}
}
Realize that this is only the additions (with some of the preceding code to support them), not replacement code for the provided src/open-wc-app.js
.
Now our initial test will pass, so itâs time to add another:
const el = await fixture(html`<open-wc-app></open-wc-app>`);
expect(el.todos.length).to.equal(0);
el.dispatchEvent(new CustomEvent('todo-new', {
detail: 'New To Do',
bubbles: true,
composed: true
}));
expect(el.todos.length).to.equal(1);
This extension of our previous test will take us beyond the initialization processes and directly into event-based state management. This means that my app will be passing data and actions via new CustomEvent()
s as triggered by dispatchEvent()
. With a lot of work as of late in front end engineering being based around virtual DOM this can often be a surprising ânewâ feature of the browser, so if youâve not gotten a chance to work with this API before Iâd highly suggest you check it out more deeply. Knowing that this is whatâs going on, we can now add code, again to src/open-wc-app.js
, in order to get our tests passing again.
constructor() {
super();
this.addEventListener('todo-new', (e) => {
let todo = e.detail;
if (todo === null) return;
this.todos = [
...this.todos,
todo,
];
});
}
My goal when doing TDD is to write code that passes, not explicitly the most beautiful code, or the most performant code, so I donât worry too much if things arenât âas they should beââ˘ď¸. Letâs agree to make room for that later, and in the interim take a look at whatâs going on here. Weâre registering an event listener against our element for the todo-new
event that our test is dispatching. When one such event is heard, weâll take the to do that weâve confirmed to be packed into the eventâs detail (e.detail
) and append it to the existing list of to-do items. Further, youâll see weâre using the spread operator to maintain the identity of our individual to-dos while renewing the identity of our todos
property which will notify LitElement
to kick off the render pipeline while still being able to compare our individual to-dos.
With our test passing again, itâs time to get back in there and complete the round trip of creating a new to do. Notice how we complete the data processing test by confirming the content of the new to-do as well as the previous test of changes to the entire todos
array. There is also a test for whether these changes are reflected in the actual rendered results.
it('adds a to do in response to a `todo-new` event', async () => {
const newTodo = 'New To Do';
const el = await fixture(html`<open-wc-app></open-wc-app>`);
expect(el.shadowRoot.querySelectorAll('to-do').length)
.to.equal(0);
el.dispatchEvent(new CustomEvent('todo-new', {
detail: newTodo,
bubbles: true,
composed: true
}));
await nextFrame();
expect(el.todos.length).to.equal(1);
expect(el.todos[0]).to.equal(newTodo);
expect(el.shadowRoot.querySelectorAll('to-do').length)
.to.equal(1);
expect(el.shadowRoot.querySelectorAll('to-do')[0].textContent)
.to.equal(newTodo);
});
Youâll see that with the addition of tests against the rendered output we put to use the first of many open-wc tools that will be of benefit to building your app. nextFrame
as acquired by import { nextFrame } from â@open-wc/testing';
is a helper method that supports working with LitElement
's asynchronous rendering process. Because rendering with LitElement
happens with micro-task timing you wonât be able to test changes to the DOM synchronously, the nextFrame
helper delays your test until the next frame so that tests against the DOM will have the newly rendered results to test against. To achieve the desired changes, the following update to the render()
method is required:
render() {
return html`
${this.todos.map(todo => html`
<to-do>${todo}</to-do>
`)}
`;
}
Et voilĂ , you have fully tested Custom Event-based management of the to-do adding process.
No, we havenât created UI or testing of the element that might dispatch such an event. However, to see our code so far working in an actual browser, visit the console and run code similar to what you see in the test to publish a to do to the page:
$0.dispatchEvent(new CustomEvent('todo-new', {
detail: 'Fancy Console Created To Do',
bubbles: true,
composed: true
}));
$0 is the currently selected node in the Elements panel.
Yes, there is a lot more to test and build, but as I mentioned before this isnât a âHow to Make a To-Do Appâ article. Iâm merely introducing the benefits of having testing built into your project from day one. Whether you leverage that by applying the tenants of test-driven development, or some other philosophy, Iâd love to hear more...drop me a note in the comments. With this capability in your project, Iâll be sending good vibes into the world that you make it further with whatever approach you choose before the excitement of coding overcomes your conviction to the process.
Note: I made it through three full tests, the above being one, before becoming unable to maintain the rigor of TDD. I was pretty proud, particularly in that those three tests covered a good amount of the application's main features. In the end, it took me 20+ tests, which is probably more than absolutely necessary, to support 100% code coverage of the final To Do app.
Pro Tip
When I do the work of testing my application (I promise I do it as much a possible) the end goal is always something like this:
However, the open-wc Starter App provides the following style of reporting by default:
I got into my test results and I was immediately struck by the questions of âWhat havenât I tested yet?â and âHow can I know where it is?â. I just wanted to see my uncovered lines. I just wanted this:
I wanted it so much, I went straight to the source and created an issue. (Issues can be a great way to thank open source projects you rely on. Not only does it build their knowledge base, and sometimes yours, it starts the conversations needed to outline the sorts of PRs that would be acceptable to the project to either solve your problem or document why certain things are true) There I was schooled on something that I had included in the settings of my testing suites since the beginning of my unit testing existence: the html
reporter. If youâve run into wanting to see coverage lines too, run some yarn test && yarn start
on your new open-wc Starter App and then checkout localhost:8080/coverage
to get your mind blown by no only a more complete coverage report than I had ever seen before, but also an interactive breakdown of what parts of your code have yet to be tested. Yes, I accept comments (below) in judgment of the things I should probably have already known, as well as for my bad jokes.
If youâre still looking to get the uncovered lines reported in your terminal, take a swing at the following changes to karma.config.js
:
module.exports = config => {
config.set(
merge.strategy(
{
'coverageIstanbulReporter.reports': 'replace',
}
)(defaultSettings(config), {
files: [
// allows running single tests with the --grep flag
config.grep ? config.grep : 'test/**/*.test.js',
],
// your custom config
coverageIstanbulReporter: {
reports: ['html', 'lcovonly', 'text']
}
})
);
return config;
};
Notice the usage of 'replace'
in the 'coverageIstanbulReporter.reports'
property, this allows you to overwrite (rather than âmergeâ, which is what the code is set to do by default) such that you arenât given both types of reports. Unless, of course, youâre into that sort of thing...more is actually more, after all.
The Short Game
As voted on by a plurality of people with opinions on such topics that are both forced to see my tweets in their Twitter feed and had a free minute this last week, a 9000+ word article is a no, no.
So, it is with the deepest reverence to you my dear reader that Iâve broken the upcoming conversations into a measly ten sections. Congratulations, youâre nearing the end of the first! If youâve enjoyed yourself so far, or are one of those people that give a new sitcom a couple of episodes to hit its stride, hereâs a list of the others for you to put on your Netflix queue:
- Not Another To-Do App
- Getting Started
- Test Early, Test Often (you are here)
- Measure Twice, Lint Once
- Make it a Component
- Make it a Reusable Part (Why arenât the installations of this series reusable? That would have been a great idea!)
- Does Your Component Really Need to Know That?
- Separate Things Early, Often, and Only as Needed
- Some Abstractions Arenât (Just) For Your App
- Reusable and Scaleable Data Management/And, in the end...
- See the app in action
Special thanks to the team at Open Web Components for the great set of tools and recommendations that theyâve been putting together to support the ever-growing community of engineers and companies bringing high-quality web components into the industry. Visit them on GitHub and create an issue, submit a PR, or fork a repo to get in on the action!
Posted on August 19, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.