-3

Suppose there's a root <App /> which houses multiple components, one of them being, say <SearchBox />.

App component houses the app state (such as searchResults array) and iterates the results underneath the <SearchBox onResults={this.onResults}/> as <SearchResult url={url} title={title}/>

Inside the SearchBox component, a call to a library is made which in turn calls a remote API, which then returns results back to onResults that is passed to the child SearchBox.

In other words, we have:

App (has state which is modified w/ onResult) -> SearchBox (receives onResult) -> API Call

and then:

API Call -> SearchBox -> <App /> (via onResult callback) -> Update state -> Render results in <App />

What is the appropriate way of testing the functionality of <SearchBox /> component given that it relies on being passed a function from the parent?

I'm thinking:

  • Mock the prop
  • Mock the search library (return some fixture)
  • Test the component "in a vacuum" and see if the mocked function runs?

Or should this be tested by instead testing the parent component and supplying an unadulterated onResults from the parent and somehow watching that it was in fact called?

What is the appropriate way to test this component setup? Is this spilling over into integration testing territory vs unit testing? (on that point, how would an integration test be setup if it relies on a call to an API which may or may not be successful?)

1

Both, probably. Within a "unit test" suite you might have:

  • Lower-level tests for SearchBox, which use test doubles for the collaborator (a service wrapping the library*) and the prop (onResults) to check that e.g.:

    1. for a given input the right call to the service was made; and

    2. for a given return from the service call the right value was passed to the callback prop.

    These are isolated tests that can run very quickly, require relatively minimal setup and can cover lots of detail within that component. However, they are also more coupled to the complement; if you decide some logic should move up to the parent or into the service, the tests have to change too.

  • A higher-level test of the App, involving the real SearchBox and service (using something like MSW to avoid making actual requests) to test everything in context.

    This is more of an integration test. Testing at this level makes it possible to refactor logic between the components involved, because the tests don't depend on where it sits. The downside is that the setup can become unwieldy when you have many components in various states making lots of different requests (all of which may succeed or fail). In a typical single-page app you might align this to the "page" level, for example, then have higher-level E2E/functional tests that use a browser driver to make sure that all of the pages work together with real requests.

Here's a diagram I put together showing how this might work for a typical web app in a TDD context:

Diagram: outside-in TDD for a web application

The idea is to start with a failing test at the top layer then move to the lower layers as you go. I didn't show an integration layer for the client app, but you can apply the same logic as shown for the server: higher-level tests first, then drill into the detail as needed.

* "Don't mock what you don't own" - it might be easy to mock out the library itself, but then you allow your app to be coupled to the library's API.

1
  • Fantastic answer and exactly what I was looking for. – McDerp Oct 7 '20 at 23:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.