← 🏠 🚶Home

Test Driven Development with Svelte - To Mock or Not to Mock

Mocking

Our test target component can interact with internal and/or external services. And sometimes in our tests, instead of interacting with those real services, we need their mock version. Sometimes we use mocks because of the complexity of the test setup, or we use them to have predictable behavior in our test.

For instance, a service used by our component can be dependent on to Math.random(). Since we cannot know what that random result will be, we cannot have predictable tests for our component.

In web applications, the frontend interacts with backend and those interactions are also unpredictable. When you run the test, there can be delays or timeouts in the request, or since we cannot control that external service state, we cannot know what kind of response will be received.

Lets see it on this example.

To Mock

For this scenario lets use a public api. https://randomuser.me/

lets create a new component, RandomUser.svelte and corresponding test module RandomUser.spec.js

and lets add our first test

// RandomUser.spec.js
import RandomUser from './RandomUser.svelte';
import { fireEvent, render, screen } from '@testing-library/svelte';
import '@testing-library/jest-dom';

describe('Random User', () => {
  it('has button to load random user', () => {
    render(RandomUser);
    const loadButton = screen.queryByRole('button', { name: 'Load Random User'});
    expect(loadButton).toBeInTheDocument();
  })
})

this first test is looking for a button on page. Lets add it. But before that make sure you run the test in console by runnin npm test

then lets add the button

<!-- RandomUser.svelte -->
<button>Load Random User</button>

Test is passing now.

Lets show this component in our application.

// main.js
import RandomUser from './RandomUser.svelte';

const app = new RandomUser({
    target: document.body
});

export default app;

open another terminal and run app npm run dev. Open http://localhost:5000

You must be seeing a button on page.

Now we are going to click to this button and it will be making an api call to randomuser.me But first lets install a library for this api call.

npm i axios@0.21.1

somehow the latest version of axios is causing trouble with the rollup. to simplify the setup, using this version.

Make sure you stop and start test and app consoles after installing a new dependency.

Lets use axios for http calls.

We are going to add our test for this requirement. But first lets see the returned object from randomuser api.

{
  "results": [
    {
      "gender": "female",
      "name": {
        "title": "Miss",
        "first": "Jennifer",
        "last": "Alvarez"
      },
      "location": {
        //
      },
      "email": "jennifer.alvarez@example.com",
      "login": {
         //
      },
      "dob": {
        "date": "1954-07-01T18:59:36.451Z",
        "age": 67
      },
      "registered": {
        "date": "2016-11-17T05:48:39.981Z",
        "age": 5
      },
      "phone": "07-9040-0066",
      "cell": "0478-616-061",
      "id": {
        "name": "TFN",
        "value": "531395478"
      },
      "picture": {
        "large": "https://randomuser.me/api/portraits/women/24.jpg",
        "medium": "https://randomuser.me/api/portraits/med/women/24.jpg",
        "thumbnail": "https://randomuser.me/api/portraits/thumb/women/24.jpg"
      },
      "nat": "AU"
    }
  ],
  "info": {
    //
  }
}

so the object we are looking for is in the results array. now lets add our test

// this test will be having async/await
it('displays title, first and lastname of loaded user from randomuser.me', async () => {
  render(RandomUser);
  const loadButton = screen.queryByRole('button', {
    name: 'Load Random User'
  });
  
  // we will click the button but our request must not be going
  // to the real server. we can't be sure how that request
  // ends up. So we will mock it. Lets make sure we set what
  // axios will return. 
  // lets define the mock function first
  // axios get, post ... functions are promise and here
  // we will mock success response by mockResolvedValue
  // and we will return the axios response object.
  // so we put the actual api response into data object here
  const mockApiCall = jest.fn().mockResolvedValue({
    data: {
      results: [
        {
          name: {
            title: 'Miss',
            first: 'Jennifer',
            last: 'Alvarez'
          }
        }
      ]
    }
  });
  // now lets assign this mock function to axios.get
  axios.get = mockApiCall;
  // then we can click the button
  fireEvent.click(loadButton);
  // and we expect to see this text on screen.
  const userInfo = await screen.findByText("Miss Jennifer Alvarez");
  expect(userInfo).toBeInTheDocument();
});

this test fails and you should be seeing a message like this

  ● Random User › displays title, first and lastname of loaded user from randomuser.me

    TestingLibraryElementError: Unable to find an element with the text: Miss Jennifer Alvarez. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

lets fix this.

<!-- RandomUser.svelte -->
<script>
  // importing axios
  import axios from 'axios';
  // having a state variable for loaded user object
  let user;
  // this function will be loading the user
  const loadRandomUser = async () => {
    try {
      const response = await axios.get('https://randomuser.me/api')
      // assigning the result to user object
      user = response.data.results[0];
    } catch (error) {
    }
  }

</script>

<!-- assign the on:click-->
<button on:click={loadRandomUser}>Load Random User</button>
<!-- and if we have user lets display it-->
{#if user}
  <h1>{`${user.name.title} ${user.name.first} ${user.name.last}`}</h1>
{/if}

after these changes test will pass.

With mocking, we have a predictable behavior in our application. If we test this on browser, we can see in each click, we receive different users.

But the downside of mocking is, now our test is highly coupled with our implementation detail. If we decide to replace axios with fetch, then our test needs to be refactored accordingly.

lets do that.

The fetch is coming with the browser. So to use it in our component we don't need to install anything. But in our test environment, jest is running the tests and it doesn't have fetch in it. So using fetch in application will cause problem on test part. To resolve this lets install another package. This is only needed for test modules.

npm i -D whatwg-fetch

now lets import this one in our test

// RandomUser.spec.js
import "whatwg-fetch";

But other than this import, lets do nothing on test. But lets use fetch on our app.

// RandomUser.svelte
<script>
//

  // lets replace axios with fetch here
  const loadRandomUser = async () => {
    try {
      const response = await fetch('https://randomuser.me/api');
      // to get the response body, we need ..
      const body = await response.json();
      user = body.results[0];
    } catch (error) {
    }
  }
</script

after these changes the tests are failing. But if we test this on browser, the user is properly loaded. So form user point of view, there is no difference. But since our test is coupled with axios usage, it is broken now. We can update our mock functions in test to make our test pass. Or we can resolve it without mocking.

Or Not To Mock

We are going to use the library Mock Service Worker - MSW Lets install it

npm i msw

We are going to use it in our test module.

// RandomUser.spec.js
// lets import these two functions
import { setupServer } from "msw/node";
import { rest } from "msw";

it('displays title, first and lastname of loaded user from randomuser.me', async () => {
// here we will create a server
  const server = setupServer(
    // and this server is going to be processing the GET requests
    rest.get("https://randomuser.me/api", (req, res, ctx) => {
      // and here is the response it is returning back
      return res(ctx.status(200), ctx.json({
        results: [
          {
            name: {
              title: 'Miss',
              first: 'Jennifer',
              last: 'Alvarez'
            }
          }
        ]
      }));
    })
  );
  // then..
  server.listen();
  // so at this step we have a server
  // after this part we don't need to deal with axios or fetch
  // in this test function
  render(RandomUser);
  const loadButton = screen.queryByRole('button', {
    name: 'Load Random User'
  });
  fireEvent.click(loadButton);
  const userInfo = await screen.findByText("Miss Jennifer Alvarez");
  expect(userInfo).toBeInTheDocument();
});

after this change, test must be passing. Now our test is not dependent onto the client we are using. We can go back and use axios again.

const loadRandomUser = async () => {
  try {
    const response = await axios.get('https://randomuser.me/api')
    user = response.data.results[0];
  } catch (error) {
  }
}

Tests must be passing with this usage too.

The mocking is a very good technique in scenarios where external services are taking place. With mocking we are able to create a reliable test environment. But the down side of it, our tests are being highly coupled with our implementation. My choice is to avoid mocking if I can. And the msw library is great replacement for backend in client tests.

Resources

Github repo for this project can be found here

You can also check this video tutorial for the same setup steps of svelte projects.

And if you would be interested, I have a full course TDD in svelte. Svelte with Test Driven Development

Thanks for reading

Svelte with Test Driven Development

Svelte with Test Driven Development

Articles

Basar Buyukkahraman

Başar Büyükkahraman

Codementor badge