Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Introduction to Testing

Steve Kinney
October 02, 2024
21

Introduction to Testing

Steve Kinney

October 02, 2024
Tweet

Transcript

  1. 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.
  2. 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.
  3. import { it, expect } from 'vitest'; it('is a super

    simple test', () = { expect(true).toBe(true); });
  4. I’ve heard of testing. But, I don’t know how to

    do it and this makes me feel bad.
  5. I’m doing it? I think? But, I still have bugs

    and my pager is still going off at night.
  6. 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
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. Let’s take a look at how I’m thinking we should

    spend our time together. What are we going to cover today?
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. 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 ?
  17. 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.
  18. 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.
  19. 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.
  20. 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.
  21. 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
  22. 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.
  23. 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.
  24. Take two of more things and test how they work

    when interacting with each other. Integration tests.
  25. Tests all of the things. Usually this involves driving a

    browser of some sort. End-to-End tests.
  26. 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.
  27. 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 🤷
  28. 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.
  29. 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.
  30. 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.
  31. 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' }); });
  32. 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.
  33. 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.
  34. 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.
  35. 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.
  36. 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.)
  37. 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.
  38. 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.
  39. 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.
  40. 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?
  41. Sometimes you want to test that a built-in function was

    called with the correct arguments.
  42. 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.
  43. 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.
  44. 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.
  45. 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.
  46. 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.
  47. 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.
  48. 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.