In this blog, you will learn how to mock APIs on Storybook using MSW. This blog will assume you have your project setup with either GraphQL, or a REST API like Axios or Fetch API and also will assume you have Storybook installed in your project. We will be covering how to mock for both GraphQL and REST API.
In this project, we will use Vue and our UI tool. But don't worry. The sample code will work for whichever framework you choose.
What is MSW?
MSW(Mock Service Worker) is an API mocking library that uses Service Worker API to intercept actual requests.
Why Mock?
Mocking helps us avoid making an actual HTTP request by using a mock server and a service worker. This, in turn, prevents any form of break in case something goes wrong with the server you would have sent a request to.
What is a Mock Server?
A mock server is simply a fake server that works as a real server to help users test and check APIs . It imitates a real API server by returning the mock API responses to the API requests. You can ream more here.
What is a Service Worker?
A Service worker enable communication between the application, the browser, and the networks (if netwrok is available). They are intended, among other things, to enable the creation of effective offline experiences, intercept network requests, and take appropriate action based on whether the network is available or not.
You can learn more about Service Worker API here.
Use Cases
Enough of all the stories ๐. Now we will be looking at two use cases:
- GraphQL
- REST API
We will need to install some pulig ins to maximize what msw has to offer by running one of these commands in the root directory of the project:
Installing MSW and the addon
# With npm
npm i msw msw-storybook-addon -D
# With yarn
yarn add msw msw-storybook-addon -D
Generate a service worker for MSW in your public folder.
npx msw init <PUBLIC_DIR>
Replace the <PUBLIC_DIR>
placeholder with the relative path to your server's public directory.
For example, the Vue command will be:
npx msw init public/
You can check here to see what path your framework will use.
Note: If you already use MSW in your project, you have likely done this before, so you can skip this step.
Configuring Storybook
In your .storybook/preview.js
file, add this:
import {
initialize,
mswDecorator
} from 'msw-storybook-addon';
// Initialize MSW
initialize();
// Provide the MSW addon decorator globally
export const decorators = [mswDecorator];
You also want to ensure that your GraphQL set up is initialized in this .storybook/preview.js
file if you are using Apollo Client.
Creating our mock API
Lets create where our mocking will be happening by first creating mock
folder in the src
folder.
Create a data.ts
file in the mock folder, and add this code, which will serve as our fake response.
export const userDetails = {
user: {
name: "Jerry Hogan",
login: "hdJerry",
bio: "I am a Front-End Developer with about 3 years plus of experience and with a very good knowledge of JavaScript, Vue, React, Tailwind, Styled component",
company: null,
avatarUrl:
"https://avatars.githubusercontent.com/u/28502531?u=c0598241c0ca3dba90aff9d21790cb330952fd94&v=4",
followers: {
totalCount: 19,
},
},
};
export const posts = [
{
userId: 1,
id: 1,
title:
"sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
},
{
userId: 1,
id: 2,
title: "qui est esse",
body: "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla",
},
{
userId: 1,
id: 3,
title: "ea molestias quasi exercitationem repellat qui ipsa sit aut",
body: "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut",
},
{
userId: 1,
id: 4,
title: "eum et est occaecati",
body: "ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit",
},
];
Create a mockedPost.ts
file and add this code.
import { rest } from "msw";
import { posts } from "./data";
export const mockedPostFetch = rest.get(
"https://jsonplaceholder.typicode.com/posts",
(_, response, context) => {
return response(context.json(posts));
}
);
Create a mockedUserProfile.ts
file and add this code.
import { graphql } from "msw";
import { userDetails } from "./data";
export const mockedUserProfileQuery = graphql.query(
"UserProfile",
(_, res, ctx) => {
return res(ctx.data(userDetails));
}
);
The concept of interception comes into place with these mocked files. For example, if there is any request outside of the mocked request, msw won't mock it. So every API url or query we want to mock must correspond to the component that's data you are trying to mock, regardless of if the API url is authentic or not.
Create a handlers.ts
file and add this code. Handlers allow us to have multiple request, no matter its method [POST, GET]...
import { mockedPostFetch } from "./mockedPosts";
import { mockedUserProfileQuery } from "./mockedUserProfile";
export const graphQlHandlers = [mockedUserProfileQuery];
export const apiHandlers = [mockedPostFetch];
How do we make use of the Handler?
I created two components to test for our two use cases
- Card
- Posts
Card component
- Create a Card folder in your component folder.
- Create a
Card.vue
and add this snippet
<template>
<div v-if="!loading" class="card">
<div class="dp">
<img :src="data.avatarUrl" alt="dp" />
</div>
<div class="user">{{ data.name }} ๐</div>
<div class="bio">{{ data.bio }}</div>
<div>Followers: {{ data.followers.totalCount }}</div>
</div>
<div v-else class="card loading">Loading...</div>
</template>
<script lang="ts">
import { USER_QUERY } from "@/graphql/user.query";
import { useQuery } from "@vue/apollo-composable";
import { defineComponent, computed } from "vue";
export default defineComponent({
name: "UserCard",
});
</script>
<script lang="ts" setup>
const { result, loading } = useQuery(USER_QUERY, {
username: "hdjerry",
});
const data = computed(() => result.value?.user ?? []);
</script>
<style scoped>
.card {
width: 20rem;
padding: 1rem;
box-shadow: 0 0 20px 2px #ccc;
border-radius: 6px;
color: #333;
min-height: 30rem;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
font-weight: 500;
}
.dp {
width: 20rem;
height: 20rem;
border-radius: 100%;
overflow: hidden;
margin: 0 0 1rem;
}
.dp > img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user {
font-size: 1.2rem;
font-weight: medium;
color: #2c3e50;
}
.bio {
border-left: 4px solid rgb(154, 28, 179);
padding: 0.2rem 0.5rem 0.2rem 1rem;
margin: 1rem 0;
background-color: #eee;
}
</style>
- create an index.ts and add this code
import Card from "./Card.vue";
export default Card;
- create Card.stories.ts and add this code
import { graphQlHandlers } from "@/mock/handles";
import Card from "./Card.vue";
export default {
title: "Components/Card",
component: Card,
argTypes: {},
};
type Templates = {
bind: (arg: unknown) => {
parameters: unknown;
};
};
const Template: Templates = () => ({
components: { Card },
template: "<Card />",
});
export const Default = Template.bind({});
Default.parameters = {
msw: {
handlers: graphQlHandlers,
},
};
below is the UI expectation
Posts component
- Create a Posts folder in your component folder.
- Create a
Posts.vue
and add this snippet
<template>
<div v-if="!loading" class="posts">
<div v-for="{ id, title, body } in data" :key="id" class="card">
<h4 class="title">{{ title }}</h4>
<div class="body">{{ body }}</div>
</div>
</div>
<div v-else class="card loading">Loading...</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "UserPosts",
});
</script>
<script lang="ts" setup>
const loading = ref(true);
const data = ref(null);
fetch("https://jsonplaceholder.typicode.com/posts")
.then((data) => data.json())
.then((res) => {
data.value = res.slice(0, 4);
loading.value = false;
});
</script>
<style>
.posts {
width: 100%;
padding: 0.5rem;
}
.card {
flex: 1;
min-height: 5rem;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: justify;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
font-weight: 500;
}
.card > .body {
font-size: 1rem;
font-weight: 200;
line-height: 1.2;
color: rgb(117, 114, 114);
word-wrap: break-word;
}
.card > .title {
text-transform: capitalize;
color: #2c3e50;
}
.card:not(:last-child) {
border-bottom: 1px solid #ccc;
}
</style>
- create an index.ts and add this code
import Posts from "./Posts.vue";
export default Posts;
- create Posts.stories.ts and add this code
import Posts from "./Posts.vue";
import { apiHandlers } from "@/mock/handles";
export default {
title: "Components/Posts",
component: Posts,
argTypes: {},
};
type Templates = {
bind: (arg: unknown) => {
parameters: unknown;
};
};
const Template: Templates = () => ({
components: { Posts },
template: "<Posts />",
});
export const Default = Template.bind({});
Default.parameters = {
msw: {
handlers: apiHandlers,
},
};
below is the UI expectation
Code Explanation
We created a card and posts component that is making use of certain query/API call to get data.
msw has a property handlers which accepts an array of requests, giving us the ability to add as many requests as we want provided that they are in the component.
Overview Of the UI
Below is an overview of the UI, which brings together the components:
Whewww!!! ๐
Why mock?
- You want test without hitting the actual API
- Yhe API isn't yet avaible from the backend team.
- During development to reduce dependencies between teams.
- To accelerate third parties API integration.
- during functional and integration testing.
- to test various advanced scenarios more easily.
- for demonstration purposes.
Conclusion
If you have any issues, we provided a repo which you can use as a template if you want to start a new project with the setup and use it to practice.
Please let me know if you have any issues or questions, and also contributions are welcome.