Comment by crdrost

Comment by crdrost 5 hours ago

0 replies

Hm. Just thinking out loud. TLDR is that I think the core of the solution to good testing of React components, would look like using it like the Web Component model?

So one thing that I keep coming back to as kind of a "baseline" is called Functional Core, Imperative Shell. It's a little hard to explain in a short space like this and the presentations available on YouTube and blog articles are a bit confusing, but basically it asks for your application to be broken into three parts: (1) modules in the "functional core" define immutable data structures and purely deterministic transformations between them; (2) modules in imperative libraries each "do one thing and do it well", they might call the SaveUser API or something like that; and (3) these are held together by a thin shell of glue code that needs to have no real logic (that's for the functional core) and needs to not do any real operations directly (that's for the libraries). And the reason to break the app apart like this is that it's test-centric: modules in (1) are unit-tested without mocks, by creating immutable data structures, feeding to the transforms, and checking the output; modules in (2) will init a real connection to a dev server upstream and are integration-tested by sending the real request to the real server; and the shell in (3) is basically so simple that it requires one end-to-end test that just makes sure that everything compiled together all right and can initialize at runtime. So this is what software looks like if you elevate Testing to be the One Core Pillar of the application, and demand "no mocks!" as part of that.

Q1, how do we apply this to a React app? Well, if you think in terms of UI, you can kind of think of an entire view as being kind of (3) as long as you aggressively oversimplify the behaviors: so you click on some part of some view and it dispatches some ButtonClicked data structure into some view-wide event queue, but it does no logic of its own. Reminds me of Redux, also reminds me a lot of Lit and web components where they just kind of emit CustomEvents but aren't supposed to do anything themselves.

Q2, how do components fit in. We have to be a little more careful there. You're talking about a modular architecture though, real thick components. Componentizing, takes us a step back from that ideal, right? It says "I don't want this to look like a single unified whole that is all tested at once, I want this to look like a composition of subsystems that are reusable and tested independently."

A simple example might be a tabbed view or accordion control, I want to coax the viewer through these N different steps, the previous step needs to be complete and then you can go to the next one. And I want the components to be each of these views. (The actual tab view or accordion view is of course another component, but it's a "thin component" in the above sense, it doesn't actually have any imperative library and the logic is relatively trivial, it doesn't generate the sorts of questions you're asking about.)

So just to roll up a random mental example, one of these tabs is some PermissionsEditor component, once you initialize it, it has everything it needs inside the component to fetch permissions from the API, fetch the current user, see what permissions the current user is allowed to grant to other users (or themselves?)... but the other tabs need to be dynamically responsive, once you add yourself to the group that can edit Flotsam and Jetsam, going to the Flotsam tab the "Edit" button should no longer be grayed out etc.

Then I think the proper way to view these thicker components, is as being inserted at level (2) into the main application? So the main application just treats them as imperative libraries, "I will give you a div and call your init_permissions_editor function with that div and you render into there. I will give you a channel to communicate events to me on. You will provide me the defs of the immutable data structures you'll send down that channel, I will provide deterministic transformations of those events into other events that I need to do."

With some caveats, yeah, I'd basically say this is the web-component model. Your external application just integration-tests that init_permissions_editor will render _something_ into a blank <div> given. Your PermissionsEditor component is responsible for integration-testing that it can create permission, add user to group, all of that, and is responsible for testing that it emits certain events when these things happen.