DEV Community

loading...

Tips for writing great Svelte tests

d_ir profile image Daniel Irvine 🏳️‍🌈 ・5 min read

In the last part in my series on Svelte testing, I’ll round off with some smaller pieces of advice.

To see all the techniques used in this series, remember to check out the demo repo on GitHub.

GitHub logo dirv / svelte-testing-demo

A demo repository for Svelte testing techniques

 Focus on behavior, not static data

Remember why we should unit test? Here’s one simple way of saying it: to avoid overtesting.

Overtesting is when you have multiple tests that cover the same surface area. It can result in brittle tests that simulatenously break when you make changes, which then slows you down as you fix all those tests.

But there are times when unit testing is overkill.

The biggest of those times is when you have static data, not data which changes.

Take, for example, a version of the Menu component that was introduced in the previous part on testing context. The version we looked at had a button that, when clicked, would open up the menu overlay.

But what if we’d like to draw a hamburger icon in place of the button. Menu.svelte might end up looking like this:

<button on:click={toggleMenu} class="icon">
  <svg
    viewBox="0 0 100 100"
    width="32"
    height="32">
    <!-- draw a hamburger! -->
    <rect x="10" y="12.5" width="80" height="15" rx="3" ry="3" />
    <rect x="10" y="42.5" width="80" height="15" rx="3" ry="3" />
    <rect x="10" y="72.5" width="80" height="15" rx="3" ry="3" />
  </svg>
</button>
{#if open}
<div class="overlay" on:click={toggleMenu}>
  <div class="box" on:click={supressClick}>
    <slot />
  </div>
</div>
{/if}
Enter fullscreen mode Exit fullscreen mode

Look at that svg there. It’s a lot of declarative, static data, to draw a 32x32 hamburger.

Any unit test you write is essentially going to repeat what’s written here: the test would verify that the HTML looks exactly like it does here.

I don’t write those tests. I see it as duplication. If HTML is static and never changes, I don’t use unit tests. If the system has a good set of system tests, then I may write them there instead.

But often I just won’t write them. I’m lazy.

This is a very different attitude from people who do not write unit tests at all. Often people don’t write unit tests because they feel that it’s too much work. In actual fact, writing unit tests but avoiding overtesting gives you less work but still gives you confidence in your code.

But now what if I wanted to introduce the ability for the caller to set their own Menu icon, by providing a named slot icon?

<button on:click={toggleMenu} class="icon">
  <slot name="icon">
    <svg
      viewBox="0 0 100 100"
      width="32"
      height="32">
      <rect x="10" y="12.5" width="80" height="15" rx="3" ry="3" />
      <rect x="10" y="42.5" width="80" height="15" rx="3" ry="3" />
      <rect x="10" y="72.5" width="80" height="15" rx="3" ry="3" />
    </svg>
  </slot>
</button>
Enter fullscreen mode Exit fullscreen mode

Now there is behavior. The behavior is that the svg doesn’t get drawn if an icon slot is defined, and does get drawn if an icon slot isn’t defined.

In this case I would test it, but possibly only as far as saying “renders an svg element when no icon slot is provided” rather than testing each individual rect.

(By the way, I’d test that with an isolated component).

Raising events

Another time that behavior is important is when raising (or firing) DOM events, like click, input, submit and so on.

One of the big changes I noticed when moving from React to Svelte is that textboxes respond to input events rather than change events.

const changeValue = (element, value) => {
  const evt = document.createEvent("Event", { target: { value } });
  evt.initEvent("input", true, false);
  element.value = value;
  element.dispatchEvent(evt);
};
Enter fullscreen mode Exit fullscreen mode

The way I deal with events is to define little helper methods like the one above, changeValue, which can be used like this:

changeValue(nameField(), "your name");
Enter fullscreen mode Exit fullscreen mode

Some events, but not all, will need to have Svelte’s tick method called.

The library svelte-testing-library has a bunch of these methods already defined. I tend to write little functions to fire the events that I need (more on that below).

Why I don’t use svelte-testing-library

Three reasons:

  1. I think it’s overkill,
  2. I think it’s too opinionated
  3. I think building it yourself is a good way to learn

The kinds of helpers that make your tests clear are often very short, simple methods, as I’ve shown in this series. They can often be written in less than 50 lines of code.

I think some of the language that’s used around testing can be toxic. There are many, many tools and techniques to testing. For me personally, choosing a tool like any of the testing-library libraries feels like lock-in. Not just to the library, but also the opinionated ways of testing.

I’ve learnt a HUGE amount about Svelte just by figuring all this stuff out, and by writing this course. Two months ago, I knew no Svelte. Now I feel like I’ve nailed it. If I had made use of svelte-testing-library that most likely wouldn’t be true.

About the best reason I’ve heard to use a framework rather than rolling your own is that it’s anti-social, meaning it’s fine if you’re an individual developer, but the moment you work on a team of professionals, this kind of behavior just doesn’t fly. Everyone has to spend time learning your hand-crafted methods, and everyone has to spend time maintaining them. By using a library, it’s someone else’s problem.

Using object factories

A final tip. I use factory-bot to build example objects for my tests. It keeps my test suites clean and tidy. For example, here’s spec/factories/post.js:

import { factory } from "factory-bot";

factory.define("post", () => ({}), {
  id: factory.sequence("Post.id", n => `post-${n}`),
  attributes: {
    content: factory.chance("paragraph"),
    tags: factory.chance("sentence")().toLowerCase().slice(0, -1).split(" ")
  }
});

export const post = () => factory.attrs("post");
Enter fullscreen mode Exit fullscreen mode

Conclusion

That’s it for this series. If you’ve enjoyed it, please consider following me and retweeting the tweet below to share the series with others.

I’ll no doubt continue to post here about Svelte as I learn more about it and how to build professional applications with it.

All feedback is welcome, even if you’ve hated this series and disagreed with everything! Send it my way, please please please! I can only improve with practice and feedback!🙏

Discussion (12)

Collapse
vuesomedev profile image
Gábor Soós

Hi, thanks for the great tutorials on Svelte!

Yesterday I've also started playing with the framework, but I've stuck with component testing.
My problem is how to test dispatched events from a Svelte component? I haven't found any example for it.

github.com/blacksonic/todoapp-svel...

Collapse
d_ir profile image
Daniel Irvine 🏳️‍🌈 Author

Hey Gábor,

You can do it by using component.$on:

  it('should notify about delete button', async () => {
    const todo = { id: 'e2bb892a-844a-47fb-a2b3-47f491af9d88', name: 'Demo', completed: false };

    const { component, getByTestId, container } = render(Item, { todo });

    let wasCalled = false;
    component.$on("remove", ()=> { wasCalled = true; });

    fireEvent.click(getByTestId('todo-remove'));

    expect(wasCalled).toBeTruthy();
});

Whether or not you’d want to do this is another question. Svelte gives you a lot of rope... dispatch is an example of that. That fact that testing is hard is a sign that there may be a better approach with the design.

(Note--a better approach with the design, not a better approach with testing. An important difference!)

The question in my mind is does Item need to know that it exists within a List? It feels like List and Item can’t exist without one another, and each has a lot of knowledge about the other one. Is there a way to reduce the coupling? For example --- can remove functionality be moved entirely to List somehow?

You could also use stores to introduce shared state between the two, and remove the need for dispatch entirely.

Let me know your thoughts :)

Collapse
vuesomedev profile image
Gábor Soós • Edited

I've tried it out, works like a charm!

One more question: is it possible to test a component with getContext without a wrapper component?

Here is an example: github.com/blacksonic/todoapp-svel...

Thread Thread
d_ir profile image
Daniel Irvine 🏳️‍🌈 Author

Glad to hear it worked! 🎉

Not sure about getContext. I vaguely recall looking into it and figuring out that getContext expects to be called as part of a render cycle of a component tree, which you can’t easily fake.

If you’re comfortable with relying on internal APIs like $on then one thing you could do is use the tutorial view for context (svelte.dev/tutorial/context-api). Choose JS output and explore the JS code that Svelte products for your component. Dig through to learn how setContext and getContext work. It might give you a solution. Failing that, use the Svelte source on GitHub. Let me know if you figure something out--I’d be interested to know.

Collapse
vuesomedev profile image
Gábor Soós

Thanks for the example, I'll try it out tomorrow.

In my opinion events (or function props) and props mean loose coupling as they only know each others interface and shared state is a strong coupling. Why do you think they know about each other? Yes, they know each other's interface, but it's necessary to communicate.

With other frameworks (Vue, Angular, React) I've found it easy to test events or function props, what I was missing is the $on method.

Thread Thread
d_ir profile image
Daniel Irvine 🏳️‍🌈 Author

Item knows about a remove operation, which means it "knows" that it exists in some kind of container (List or otherwise).

Not saying I know of a better solution or that I wouldn’t write it that way myself... just that I’m a little wary of dispatch and I’d keep my eyes open for opportunities to design it differently.

Thread Thread
d_ir profile image
Daniel Irvine 🏳️‍🌈 Author

Another point: unless I’m mistaken, $on is part of the internal Svelte API so if you use it, you have to be prepared to figure out a new solution if/when the maintainers break this one 🤣

Collapse
jeffwscott profile image
Jeff Scott

I'm new to testing front-end. It's amazing how complicated it is and how uncomplicated everyone makes it out to be. If you visit any of the testing framework websites they would have you believe you are just an npm install and 5 lines of code away from testing. It's the biggest lie in front-end development.

When you develop a true production app (not a webpage) you have information coming from so many vectors that all shape the current state of the UI. Local storage hydrating state, fetches, props, etc.

For example, I have a rather large svelte application with an entry-point for App.svelte. The first thing the user sees is wholly dependent on the svelte stores and what data they are pulling from local storage. If there is nothing in the settings key then they see a first time setup screen flow. If settings is there then they see a main page, or the previous page they left off on. That previous page maybe be a child page and have props.

A good functioning app needs to take all of these things into consideration and I don't see a SIMPLE way to test it all.

My ideal testing framework would make it easy and simple to test each component individually and have you setup the state contexts and then mount the component. I just want to do this every time.

For Each Test:

  • Mock Local Storage
  • Mock fetch responses (these are called stubs or something??)
  • Mock Prop state
  • Mount Component
  • Run Tests
  • UnMount

You're probably going to say that this is possible. And you might even say that the articles I just read show me how to do it. That may be true but I just finished creating a complex app in Svelte using many technologies I had to learn and had to overcome daily challenges to get it out the door. None of that knowledge or syntax seems to translate over to the testing at all. Instead I seem to have to learn how to wire 5 more things together, install a bunch more dependencies and learn a whole new syntax and methodology.

This isn't an indictment on your series. I will no doubt use the information in these articles to test like 15% of my app because it's all I'm going to be able to cobble together in the short amount of time I have allotted for testing (just being real). This is just a venting of frustration for what is involved to test something in the first place because, given Svelte's mandate, I'm disappointed that the state of testing is so disjointed.

I appreciate people like YOU that can pick up the pieces, put together the puzzle and communicate the picture to clueless devs like myself. Lots of love we need people like you in the Svelte community.

Now to start figuring out how to implement roll-up for my testing after just creating an entire app with webpack (FML).

Collapse
d_ir profile image
Daniel Irvine 🏳️‍🌈 Author

Hey Jeff, thank you for this. I think your comment will resonate with many people, especially those who’ve been successfully testing back-end codebases and then moved to the front-end.

I know you’ve already built your app, but the best advice for testable front-end codebases is this: Keep your components as simple as possible. Think of hexagonal architecture, ports-and-adapters, etc. Do any local storage access or fetch requests OUTSIDE of your components. That way your components become smaller in relative size and you might even choose to not unit test them at all.

Unit testing components is so hard that people prefer to use system/acceptance tests instead. IMHO this then results in overtesting and brittle tests, and end up costing you more in the long run.

I’d encourage you to keep exploring and to keep posting about your experiences. I for one would love to hear more as you progress, and I’m sure others would too.

Collapse
peerreynders profile image
peerreynders • Edited

Think of hexagonal architecture, ports-and-adapters, etc.

I think this is a related concept: Segregated DOM

Now 6 years ago the motivation was to be able to test functionality without the DOM but I think the benefit of a boundary between DOM (& events) and view/application state can still exist today - i.e. there can be a case to keep your (view) components thin.

Collapse
jeffwscott profile image
Jeff Scott • Edited

Hey thanks for the response.

I'm not sure what you mean by keep components as simple as possible.

Basically local storage is hydrated to stores and the stores are imported to my components. That's the recommended way for a svelte app.

I'm not 100% what I even need to test.

TBH early on I was able to implement cypress. And I did something people don't recommend, but worked for me, which was to load the app and step through it in code. This at least verified for me that things like first time setup would run. Problem was that to test any work-flow each test case would have to initially run the "first time setup" and then next test the workflow I wanted.

Unfortunately I installed an npm package (Monaco_Editor) that I need and it seems to have broke cypress and now I'm stuck with a generic cypress error and no test will run at all.

This leaves me with no avenue for support. Cypress points to Svelte or Monaco-Editor, Monaco-Editor is going to point to the other two.

It's just a mess.

I decided to try and scrap cypress and just try and test the components themselves opposed to a "workflow". But I don't think that's possible... I might look into just testing the stores themselves without the components because they do drive the app. Any suggestion on how I can do that or where I would look to find out?

Thanks again.

Thread Thread
d_ir profile image
Daniel Irvine 🏳️‍🌈 Author

I completely missed your reply 🤦‍♂️ It’s over a month later now -- let me know if you’ve made any more progress.

If all your business logic is in stores then it’d be perfectly reasonably to not test components at all, or just have a few end-to-end tests rather than unit tests.

Forem Open with the Forem app