4 ms·
Here's what I don't quite understand about mocking in Javascript. When I write a frontend application using components (React, Preact, web components — doesn't
by azangru 4y ago
Here's what I don't quite understand about mocking in Javascript.
When I write a frontend application using components (React, Preact, web components — doesn't really matter), I will often find myself in a situation when I have a component tree like this:
A --> B --> C
In which I want to test component A; but one of its grand(-grand-etc.-)children down the tree (in this case, component C) is quite complex; and setting up a test for component A in such a way as to accommodate for the dependencies of component C would be cumbersome.
Normally, I would just mock component C out entirely, using Jest's module mocking ability. However, this comes at a cost. ES6 import statements are static; and thus, when Jest finally completes its transition to ES6 modules, it won't be able to just mock plain static imports. The likely solution is that the rules for mocking will change; and if one wants to mock modules, he would have to do so via dynamic imports. Which means I'll have to go through all my tests and rewrite them :-(
I have not yet seen a good and clean story for mocking Javascript modules. A more standards-compliant project, Web Test Runner, does module mocking via import maps [0]; but this looks both clunky and not quite suited for per-test mocking.
Which leaves me confused. If mocks are evil, how do I test my components? And if they are fine, what's the future-proof standards-compliant way of module mocking in Javascript?
0 - https://modern-web.dev/docs/dev-server/plugins/import-maps/ https://modern-web.dev/docs/dev-server/plugins/import-maps/
- MrPatan 4y agoI don't unit-test ui components. Putting complex logic in ui components is a mistake anyway. Do test end to end.
- JyB 4y ago> Do test end to end. What do you use concretely (and how) for this?
- rockwotj 4y agoCheckout https://playwright.dev https://playwright.dev
- sethammons 4y agoCheck out Cypress.io
- williamcotton 4y agoPuppeteer works very well. There is headless Chrome support in RSpec as well. Here’s an example using dependency injection and puppeteer: https://github.com/williamcotton/williamcotton.com/blob/master/test/app.test.js https://github.com/williamcotton/williamcotton.com/blob/mast...
- azangru 4y agoThere must be something better than loading the whole page, potentially with all sorts of database or network mockery required to provide it with all the data that it needs for rendering, in order to test how a particular button works. It's no coincidence that the release of Jest and Enzyme around 2015 produced such a huge enthusiasm in the frontend community.
- WorldMaker 4y agoThere's always the "Storybook approach" alternative: make sure all your UI components are individually renderable by pure props and then build standalone from your application story boards around those prop states. I tend to find that more useful for UI testing than unit tests because it gives designers something to critique and often better fits how they want to work. (Versus trying to assert things like specific DIVs exists, which might get rearranged or eliminated in the next design change.) It's easier to storyboard some complex component trees sometimes too.
- Vinnl 4y agoHmm, I'm not sure I follow the part of static imports, but I tend to only mock code that perform side effects (usually, HTTP requests), and I explicitly isolate those to separate modules that I can mock. Other than that, they're more integration tests using testing-library [1] to simulate user interactions and verifying that they have the expected effects. When there's complex business logic, I'll separate that out into a separate pure function, and I write tests for that function without needing mocks. [1] https://testing-library.com/ https://testing-library.com/
- azangru 4y ago> Hmm, I'm not sure I follow the part of static imports ====== COMPONENT FILE import PeskyChild from './PeskyChild'; // THIS IS A STATIC IMPORT. STATIC IMPORTS IN ES6 ARE HOISTED const MyComponent = () => (<div><PeskyChild /></div>); ====== TEST FILE import { render } from '@testing-library/react'; import MyComponent from './MyComponent'; // THIS ATTEMPTS TO MOCK A STATIC IMPORT. IT WON'T WORK SOON-ISH jest.mock('./PeskyChild', () => () => <div data-test-id="mocked-child-component" />) describe('MyComponent', () => { it('is awesome', () => { render(<MyComponent />) }); });
- Vinnl 4y agoRight, I guess it's the part where it will stop working that I'm not aware of :) But still, in that case I'm not sure why you'd need to mock <PeskyChild>. It's part of what the user is expecting to see, after all. (I used to see unit tests as distinct from integration tests, but really, it's pretty arbitrary where to draw the line around a "unit". If I refactor some lines out of a function/component into a separate file, should that now suddenly have its own unit tests, and be mocked out of its parent?)
- azangru 4y ago> But still, in that case I'm not sure why you'd need to mock <PeskyChild>. Not every child is a pesky one; but suppose it makes a network request, or tries to read something from IndexedDB, or relies on window.matchMedia or IntersectionObserver. And you don't want to deal with this stuff when you are testing one of its (grand)parents.
- Aeolun 4y agoIf your component has a hard dependency on B, then there’s not much you can do there if you want to test it. But if not you could make the component B injected in A dynamic (e.g. specified as a prop/children) and leave it out entirely in your test. For testing things from a functional perspective I really like Cypress component testing (actually, I really hate it, but it’s the best thing I’ve found).
- azangru 4y ago> But if not you could make the component B injected in A dynamic (e.g. specified as a prop/children) and leave it out entirely in your test. I've heard that advice before; this is why I made my example a bit more complicated, by saying that I want to swap out C (or D, or E, or whatever great-grand-child might be the troublesome one). The ultimate setup, hypothetically, could look something like this: import TroublesomeChild from './TroublesomeChild'; const ComponentA = () => ( <div> <ComponentB /> {props.children} </div> ); const ComponentB = () => ( <div> <TroublesomeChild /> {props.children} </div> ); One could advocate for passing `TroublesomeChild` to component B as a dedicated prop to make component B more testable; but if we are testing component A, then we would also have to pass component B in a dedicated prop. And what do we do if there are multiple troublesome children? Creating dedicated properties for them is neither idiomatic from component authoring perspective, nor very scaleable.
- Aeolun 4y ago> why I made my example a bit more complicated It doesn’t really change that you need to be able to test every component in isolation. For A this means you need to be able to swap B, for B this means you need to be able to swap C, etc. > Creating dedicated properties for them is neither idiomatic from component authoring perspective, nor very scaleable. I’d say if you are trying to do this for more than 3 components in the same parent you are probably doing something wrong with your structure in the first place.
- leidenfrost 4y agoI have been considering replacing static dependencies with a simple IoC container for simple services because of that. that way I can just instance a FakeRepository, inject it into RealService, and then inject it into RealController. That way you can create an integration testing way more straightforward