Slide 1

Slide 1 text

Steve Kinney, @stevekinney Introduction to Testing A Frontend Masters course.

Slide 2

Slide 2 text

Writing tests isn’t hard. But it’s easy write code that’s hard to test. We’re going to look at how to write a test or two. But, we’re also going to look at how to write code that’s easy—or at least easier—to test. And, we’ll look at how to test code that’s hard-to-test.

Slide 3

Slide 3 text

What are the prerequisites? Do you JavaScript? Then, we’re good. I’m not going to assume you’ve written a single test before in your life. We’ll test some components that happen to be written in a framework like React or Svelte. But, I don’t expect that you’ll have any familiarity with any of these. As long as you know some JavaScript, have Node installed, and get around the command line—we should be good.

Slide 4

Slide 4 text

import { it, expect } from 'vitest'; it('is a super simple test', () = { expect(true).toBe(true); });

Slide 5

Slide 5 text

it('is a super simple test', () = { expect(add(2,2)).toBe(4); });

Slide 6

Slide 6 text

There are a few stages in everyone’s testing journey.

Slide 7

Slide 7 text

Testing? I've never heard of it.

Slide 8

Slide 8 text

I’ve heard of testing. But, I don’t know how to do it and this makes me feel bad.

Slide 9

Slide 9 text

I’m doing it? I think? But, I still have bugs and my pager is still going off at night.

Slide 10

Slide 10 text

I’m going to test all the things. 100% of the way. And I’m going to hassle everyone that they’re not writing enough tests and I’m never going to approve a PR ever again and I heard that Jeff Bezos said that the build shouldn’t pass if the test coverage drops and I’d like to argue you about the difference between unit, integration, and end-to-end tests. Do you even know what it means to be idempotent? Who cares is all of my code is unreadable? Did I tell you I’ve writing my own functional programming language? Stage Four: The Danger Zone

Slide 11

Slide 11 text

I have a healthy relationship with testing. I write tests to give myself con fi dence that my code is working as expected. I don’t take it too far.

Slide 12

Slide 12 text

The end goal of testing. Why even bother? Spoiler: It’s not 100% test coverage. It’s about getting rid of that feeling of existential dread when go to refactor your code.

Slide 13

Slide 13 text

Someone is always testing your code. Hopefully, it’s you. It’s either you or it’s your users. And if it’s you, then you’re either doing it manually every time you make a change—or, you have an automated system in place. My job is to help you get that automated system in place.

Slide 14

Slide 14 text

A bias towards action. We’re going to learn about testing by writing tests—not talking about it. A lot of the content around testing has a tendency to get really philosophical. We’re going to focus on learning how to write tests by writing tests. And, we’ll touch upon all of that other stu ff as we go along.

Slide 15

Slide 15 text

Let’s take a look at how I’m thinking we should spend our time together. What are we going to cover today?

Slide 16

Slide 16 text

We’re going to write some super simple tests. Suspiciously simple, to be clear. We’re going start at the very basics. But. don’t worry—things will escalate from there.

Slide 17

Slide 17 text

We’re going to handle edge cases & errors. Other people’s code breaks—not mine. It would be great if we only hard to worry about when things go exactly as planned. But, they don’t always do that and we need to be prepared for that.

Slide 18

Slide 18 text

We’re going to look at some little tricks. Write better tests with this one weird trick. Are you going to use these tricks every day? Probably not. Do I expect you to memorize them? I don’t. I just want you to be aware that they exist if you need them.

Slide 19

Slide 19 text

We’re going to look at testing the DOM. An unfortunate reality of being a front-end engineer. I’ve been told that a lot of JavaScript developers work on UIs. I’ve been told that browser’s have this thing called the DOM. We should probably test that.

Slide 20

Slide 20 text

How to deal with stu ff you don’t control. Which is more things than I’d like, frankly. I’ve also heard that some of these web applications make network requests. Did you know you can install packages from npm? How do we test this kind of stu ff ?

Slide 21

Slide 21 text

We’re going to look at browser-based tests. This is probably a topic in it’s own right. When all us fails, we can just have our tests grab ahold of a browser. Let’s look at how to truly test from the user’s perspective.

Slide 22

Slide 22 text

There’s course website. https://stevekinney.net/courses/testing

Slide 23

Slide 23 text

There is also a repository of examples. https://github.com/stevekinney/introduction-to-testing

Slide 24

Slide 24 text

Tools of the trade. What are some of the tooling out in the world? We’re going to use a test runner called Vitest. But, it doesn’t matter. They’re all pretty much the same. It’s what I use on a daily basis and you probably want me using the tools that I’m most comfortable with for the next few hours.

Slide 25

Slide 25 text

Example A basic test. examples/scratchpad

Slide 26

Slide 26 text

It’s hard to write code that’s hard to test if you start with the tests and make them pass. On the topic of test-driven development.

Slide 27

Slide 27 text

I don’t do it 100% of the time and I don’t trust anyone who says they do. It’s good and you should do it when it’s appropriate.

Slide 28

Slide 28 text

It’s not always easy to start with tests. Don’t let anyone make you feel bad about this. Sometimes, you need to mess around and fi nd out before you settle on your fi nal approach. And, this tends to lead you writing some tests after the fact

Slide 29

Slide 29 text

Example Basic addition. examples/arithmetic

Slide 30

Slide 30 text

Exercise Subtraction, multiplication, and division. examples/arithmetic

Slide 31

Slide 31 text

And now…

Slide 32

Slide 32 text

Steve’s Rules of Testing. They’re more like guidelines. Writing tests isn’t hard. But, some code is hard to test. Your tests don’t pass because your code works. They pass because they didn’t fail. Someone is always testing your code. No one has ever broken their code into too many, small, well-named, easy-to-test functions.

Slide 33

Slide 33 text

Testing Terminology

Slide 34

Slide 34 text

The types of tests you’ll meet.

Slide 35

Slide 35 text

Take one function or object and test it in isolation. This is where we’re going to spend a lot of our time today. Unit tests.

Slide 36

Slide 36 text

Take two of more things and test how they work when interacting with each other. Integration tests.

Slide 37

Slide 37 text

Tests all of the things. Usually this involves driving a browser of some sort. End-to-End tests.

Slide 38

Slide 38 text

The Unhappy Path™

Slide 39

Slide 39 text

The Unhappy Path. Or, the importance of pessimism. Sure passing some math equations some numbers does what we expect. But, what about all of the other weird stu ff that can get in there? What about unde fi ned? How about a string? Testing the unhappy path is about making sure that you’ve thought through all of the stu ff that can get weird.

Slide 40

Slide 40 text

true + true = 2 1 + '1' = '11' NaN ! NaN

Slide 41

Slide 41 text

add(1); add(null, 1); add('1', 2); add(2, 'potato'); subtract('1', 1); divide(5, 0);

Slide 42

Slide 42 text

What to do when things go wrong. Three outcomes; only one wrong answer. Fail gracefully. Flip a table and throw an error. Let things play out as they will 🤷

Slide 43

Slide 43 text

Example Convert string to number. examples/utility-belt

Slide 44

Slide 44 text

Exercise Mathematical edge cases. examples/arithmetic

Slide 45

Slide 45 text

All Things Being Equal

Slide 46

Slide 46 text

Referential equality. Just because they’re basically the same, it doesn’t mean they’re actually the same. Sure, 1 === 1 and ‘string’ === ‘string’. But, { foo: 1 } !== { foo: 1 } and [1,2,3] !== [1,2,3]. This is where toBe, toEqual, and toStrictEqual all come in.

Slide 47

Slide 47 text

Example Strictly speaking. examples/strictly-speaking

Slide 48

Slide 48 text

toBe versus toEqual. The same in memory versus effectively the same. toBe is useful for comparing primitive values that would = each other. toEqual looks at the contents of an object or array to see if the values are equal to each other.

Slide 49

Slide 49 text

toEqual versus toStrictEqual. How equal is equal? toEqual checks if two objects or arrays have the same values and structure, allowing for loosely de fi ned properties (e.g., undefined properties are not strictly compared). toStrictEqual ensures a more precise match, where even unde fi ned properties, types, and object prototypes must exactly match.

Slide 50

Slide 50 text

it(‘has what we are re looking for', () = { expect(new Person('Alice')).toEqual({ name: 'Alice' }); }); test('strictly equal or no good’, () = { expect(new Person('Alice')).not.toStrictEqual({ name: 'Alice' }); });

Slide 51

Slide 51 text

Example Asymmetric Matchers. examples/characters

Slide 52

Slide 52 text

Exercise Characters. examples/characters

Slide 53

Slide 53 text

Before and After

Slide 54

Slide 54 text

Doing stu ff before and after each test. Not to be confused with those other kinds of hooks. If you realize that you’re doing the same thing before and after every test or you want to do something before all of the tests and then clean up after all of the tests are done, then you can use hooks. But, be warned: They’re convenient, but convenience sometimes comes at the cost of clarity.

Slide 55

Slide 55 text

Example Counter. examples/arithmetic

Slide 56

Slide 56 text

Example Refactoring the Person tests. example/characters

Slide 57

Slide 57 text

Testing Asynchronous Code

Slide 58

Slide 58 text

Testing asynchronous code. It used to be tricky; now, it’s not. This use to be a bit more of a pain. But, basically, if you remember to use async and await, you should be mostly good. But, yea—you have to remember to use async and await.

Slide 59

Slide 59 text

Testing the DOM

Slide 60

Slide 60 text

Testing the DOM. The potential problem. Your tests run in Node. Node isn’t a browser. This means it doesn’t have any of the Browser APIs. The DOM is one of those APIs. This means that Node doesn’t have the DOM. This means you can’t test the DOM from Node.

Slide 61

Slide 61 text

We’re going to do it anyway.

Slide 62

Slide 62 text

Using a DOM library. Simulate the DOM to get around this problem. Out of the box, Vitest supports two DOM libraries: JSDOM and Happy DOM. HappyDOM is small and lightweight. JSDOM is an industry standard, but it’s a heavier tool. It probably doesn’t matter which one you pick.

Slide 63

Slide 63 text

Some caveats. There is no such thing as a free lunch. • It’s still not a real browser. You're not getting every subtlety of a speci fi c Chrome, Safari, or Firefox version. It's designed to act like a browser. • Running tests with jsdom can be a bit slower. It’s the cost of emulating browser stu ff . • You might still run into browser-speci fi c issues. Just because something works in Browser Mode doesn’t mean it’ll work in all browsers. (I’m looking at you, Safari.)

Slide 64

Slide 64 text

Setting Up the Environment Just tell Vite that you want to use a DOM library. It’s as easy as tweaking one small con fi guration. You can also do it on a per- fi le basis.

Slide 65

Slide 65 text

export default defineConfig({ test: { globals: true, environment: 'happy-dom', setupFiles: ['@testing-library/jest-dom/vitest'], }, });

Slide 66

Slide 66 text

Example Testing a button. examples/element-factory

Slide 67

Slide 67 text

Exercise Testing a login form. Part I. examples/element-factory

Slide 68

Slide 68 text

Exercise Testing local storage. examples/element-factory

Slide 69

Slide 69 text

Querying. It’s kind of like jQuery of document.querySelector, but you’re trying to do it from the perspective an assistant device. This is basically a way to trick yourself into writing accessible components.

Slide 70

Slide 70 text

Example Switching tabs. Testing Library can extend the built-in matchers in order to make your life easier. You can either import these on a per- fi le basis—or just do it globally with a setup fi le.

Slide 71

Slide 71 text

Exercise Counting accidents.

Slide 72

Slide 72 text

Test Doubles

Slide 73

Slide 73 text

Faking it. Mocking and spying. If a unit test is supposed to where we test something in isolation? Then how do we make sure it’s actually isolated?

Slide 74

Slide 74 text

Sometimes you can’t control everything.

Slide 75

Slide 75 text

Sometimes you want to test that a built-in function was called with the correct arguments.

Slide 76

Slide 76 text

Sometimes you don’t want randomness to break your tests… randomly.

Slide 77

Slide 77 text

Sometimes you’d prefer not to make actual network requests to your server.

Slide 78

Slide 78 text

Sometimes your server isn’t even running.

Slide 79

Slide 79 text

Sometimes, you’re working with time and the times—they are a-changing.

Slide 80

Slide 80 text

Test doubles. Secret agents for your tests. Mocks, spies, and stubs. These are fake methods and values that you can use so that you can pin down the thing you’re actually trying to test.

Slide 81

Slide 81 text

Putting Things Back Leave no trace. • Clear: You’ve created some complex mock logic, and now you're retracing steps, clearing call history to test cleanly. • Reset: You made a mess with return values or .mockImplementation—and now you just want to start over without rebuilding the mock. • Restore: You’re done mocking, you want to reinstate the original functionality, and walk away like nothing ever happened.

Slide 82

Slide 82 text

Putting Things Back Leave no trace. • fn.mockClear(): Clears out all of the information about how it was called and what it returned. This is e ff ectively the same as setting fn.mock.calls and fn.mock.results back to empty arrays. • fn.mockReset(): In addition to doing what fn.mockClear(), this method replaces the inner implementation with an empty function. • fn.mockRestore(): In addition to doing what fn.mockReset() does, it replaces the implementation with the original functions.

Slide 83

Slide 83 text

Putting Things Back Leave no trace—in bulk. • vi.clearAllMocks: Clears out the history of calls and return values on the spies, but does not reset them to their default implementation. This is e ff ectively the same as calling .mockClear() on each and every spy. • vi.resetAllMocks: Calls .mockReset() on all the spies. It will replace any mock implementations with an empty function. • vi.restoreAllMocks: Calls .mockRestore() on each and every mock. This one returns the world to it's original state.

Slide 84

Slide 84 text

Example Spying on console.log. examples/scratchpad

Slide 85

Slide 85 text

Exercise Spying on alert. examples/element-factory

Slide 86

Slide 86 text

Exercise Preventing randomness. examples/guessing-game

Slide 87

Slide 87 text

Exercise Revisiting that login form. examples/element-factory

Slide 88

Slide 88 text

Spying on and mocking existing functions.

Slide 89

Slide 89 text

Mocking environment variables.

Slide 90

Slide 90 text

Example Let’s look at a function that should only log in development. examples/log-jam

Slide 91

Slide 91 text

Exercise Enforcing an API key. examples/log-jam

Slide 92

Slide 92 text

Mocking dependencies. You’ve got to draw the line somewhere. You don’t want to or need to test other people’s code. Sometimes that code has side e ff ects. It might make network requests. It might read or write to the fi le system. You don’t want to have to deal with any of that—nor should you.

Slide 93

Slide 93 text

Example Mocking sendToServer in Log Jam. examples/element-factory

Slide 94

Slide 94 text

Exercise Mocking data fetching. examples/element-factory

Slide 95

Slide 95 text

Time traveling.

Slide 96

Slide 96 text

Example Stop and start time. examples/scratch-pad

Slide 97

Slide 97 text

Mock Service Worker

Slide 98

Slide 98 text

Let’s look at using Mock Service Worker. Just mock out the whole network stack already.

Slide 99

Slide 99 text

Exercise Mock out a request with Mock Service Worker. examples/directory

Slide 100

Slide 100 text

Dependency Injection

Slide 101

Slide 101 text

Too much mocking? Try dependency injection! Just pass in the things you need. This is one of those areas, where just refactoring your code can make it easier to test. If your code relies on functionality that you pass in, then it’s a lot easier to pass in things that suit your purpose. I’ve never seen breaking your code into too many small, well-named, easy-to-test functions.

Slide 102

Slide 102 text

Example Dependency injection. examples/directory

Slide 103

Slide 103 text

Testing from the browser perspective.

Slide 104

Slide 104 text

Example Testing our task list. examples/task-list

Slide 105

Slide 105 text

Exercise Testing our accident counter. examples/accident-counter

Slide 106

Slide 106 text

Avenues for Further Study

Slide 107

Slide 107 text

Onwards… There is still more to learn. So, we also have this course called Enterprise UI Development. We go deeper into some of the topics we covered today. We also cover. Running up you tests with a pre-commit hook. Running your tests in a continuous integration environment with Github Actions. Setting up a code coverage tooling.