I have a login form. When I fill out the login form with data and the login button is clicked:
<flash-message>
componentSince this component heavily depends on the Vuex store, I'm unable to think of some valid test cases for this component.
I don't have experience with JavaScript ecosystem, so a verbose explanation would be appreciated.
Login.vue
<template>
<b-col sm="6" offset-sm="3">
<h1><span class="fa fa-sign-in"></span> Login</h1>
<flash-message></flash-message>
<!-- LOGIN FORM -->
<div class="form">
<b-form-group>
<label>Email</label>
<input type="text" class="form-control" name="email" v-model="email">
</b-form-group>
<b-form-group>
<label>Password</label>
<input type="password" class="form-control" name="password" v-model="password">
</b-form-group>
<b-btn type="submit" variant="warning" size="lg" @click="login">Login</b-btn>
</div>
<hr>
<p>Need an account? <b-link :to="{name:'signup'}">Signup</b-link></p>
<p>Or go <b-link :to="{name:'home'}">home</b-link>.</p>
</b-col>
</template>
<script>
export default {
data () {
return {
email: '',
password: ''
}
},
methods: {
async login () {
this.$store.dispatch('login', {data: {email: this.email, password: this.password}, $router: this.$router})
}
}
}
</script>
Vue test utils documentation says:
[W]e recommend writing tests that assert your component's public interface, and treat its internals as a black box. A single test case would assert that some input (user interaction or change of props) provided to the component results in the expected output (render result or emitted custom events).
So we shouldn't be testing bootstrap-vue components, that's the job of that project's maintainers.
To make it easier to test components, scoping them to their sole responsibility will help. Meaning that the login form should be its own SFC (single file component), and the login page is another SFC that uses the login form.
Here, we have the login form isolated from the login page.
<template>
<div class="form">
<b-form-group>
<label>Email</label>
<input type="text" class="form-control"
name="email" v-model="email">
</b-form-group>
<b-form-group>
<label>Password</label>
<input type="password" class="form-control"
name="password" v-model="password">
</b-form-group>
<b-btn type="submit" variant="warning"
size="lg" @click="login">
Login
</b-btn>
</div>
</template>
<script>
export default {
data() {
return { email: '', password: '' };
},
methods: {
login() {
this.$store.dispatch('login', {
email: this.email,
password: this.password
}).then(() => { /* success */ }, () => { /* failure */ });
}
}
}
</script>
I removed the router from the store action dispatch as it's not the store responsibility to handle the redirection when the login succeeds or fails. The store shouldn't have to know that there's a frontend in front of it. It deals with the data and async requests related to the data.
Test the store actions individually. Then they can be mocked completely in components.
Here, we want to make sure the store does what it's meant to do. So we can check that the state has the right data, that HTTP calls are made while mocking them.
import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import storeConfig from '@/store/config';
describe('actions', () => {
let http;
let store;
beforeAll(() => {
http = new MockAdapter(axios);
store = new Vuex.Store(storeConfig());
});
afterEach(() => {
http.reset();
});
afterAll(() => {
http.restore();
});
it('calls login and sets the flash messages', () => {
const fakeData = { /* ... */ };
http.onPost('api/login').reply(200, { data: fakeData });
return store.dispatch('login')
.then(() => expect(store.state.messages).toHaveLength(1));
});
// etc.
});
The only real thing this component do is dispatching the login
action when the submit button is called. So we should test this. We don't need to test the action itself since it's already tested individually.
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import LoginForm from '@/components/LoginForm';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Login form', () => {
it('calls the login action correctly', () => {
const loginMock = jest.fn(() => Promise.resolve());
const store = new Vuex.Store({
actions: {
// mock function
login: loginMock
}
});
const wrapper = mount(LoginForm, { localVue, store });
wrapper.find('button').trigger('click');
expect(loginMock).toHaveBeenCalled();
});
});
In that same vein, we should mock the store state with injected messages and make sure that the FlashMessage
component displays the messages correctly by testing the presence of each message items, the classes, etc.
The login page component can now be just a container, so there's not much to test.
<template>
<b-col sm="6" offset-sm="3">
<h1><span class="fa fa-sign-in"></span> Login</h1>
<flash-message />
<!-- LOGIN FORM -->
<login-form />
<hr>
<login-nav />
</b-col>
</template>
<script>
import FlashMessage from '@/components/FlashMessage';
import LoginForm from '@/components/LoginForm';
import LoginNav from '@/components/LoginNav';
export default {
components: {
FlashMessage,
LoginForm,
LoginNav,
}
}
</script>
mount
vs shallow
The documentation on shallow
says:
Like
mount
, it creates aWrapper
that contains the mounted and rendered Vue component, but with stubbed child components.
Meaning that child components from a container component will be replaced with <!-- -->
comments and all their interactivity won't be there. So it isolates the component being tested from all the requirements its children may have.
The inserted DOM of the login page would then be almost empty, where the FlashMessage
, LoginForm
and LoginNav
components would be replaced:
<b-col sm="6" offset-sm="3">
<h1><span class="fa fa-sign-in"></span> Login</h1>
<!-- -->
<!-- LOGIN FORM -->
<!-- -->
<hr>
<!-- -->
</b-col>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With