from theory into practice
github.com/andrewbeng89
andrewbeng89.me
Integration
Unit
E2E
the smallest, testable part of a software
/** The factorial! 😂 function
* i.e. the product of all positive integers
* less than or equal to n
*/
const factorial = n => {
if (n === 1) {
return 1;
}
return n * factorial(n - 1);
};
components/MyUiComponent.vue
<template>
<!-- Vue template markup -->
</template>
<script>
export default Vue.extend({
// Vue component logic
props: { /* some props */ },
methods: { /* some methods */ }
});
</script>
<style>
// Vue component styling
</style>
const state = { myList: [] };
const addListItem = (
state, listItem
) => {
state.myList = [
...state.myList,
listItem
];
};
const mutations = { addListItem };
export default {
state,
mutations,
namespaced: true
};
store/list.js (vuex module)
Mid-to-high complexity Vue.js components typically look this...
<template>
<div>
<button @click="fetchList" />
</div>
</template>
<script>
export default Vue.extend({
// Vue component logic
props: { /* some props */ },
methods: {
...mapActions("list", [
"fetchList"
]
}
});
</script>
<style>
// Vue component styling
</style>
mapping getters/actions from a vuex store
test input
assert output
{ application code (units, mocks) }
approach used for unit testing
test input
assert output
{ application code }
running software (staging env.)
approach used for E2E testing
test input
assert output
{ application code }
running software (staging env.)
approach used for integration testing
/** The factorial! 😂 function
* i.e. the product of all positive integers
* less than or equal to n
*/
const factorial = n => {
if (n === 1) {
return 1;
}
return n * factorial(n - 1);
};
// Assertion that factorial! is working
it("Should return the nth factorial", () => {
expect(factorial(5)).toBe(120);
expect(factorial(10)).toBe(3628800);
});
input
output
assert
execute
// A simple vuex store: list.js
const state = { list: [] };
const mutations = {
addItem: (state, item) => {
state.list = [...state.list, item]
}
};
const getters = {
getItem: state => index =>
state.list[index]
};
export default {
state,
mutations,
getters
};
// main.js
const store = new Vuex.Store(list);
dynamic getter
<!-- Vue component using the mapped getter -->
<template>
<div>
<span>
The first item is {{ getItem(0) }}
</span>
<button @click="addItem({ foo: 'bar' })">
Add Item
</button>
</div>
</template>
<script>
import Vue from "vue";
import { mapGetters, mapMutations } from "vuex"
export default Vue.extend({
computed: {
...mapGetters(["getItem"])
},
methods: {
...mapMutations(["addItem"])
}
});
</script>
curried function is mapped to a dynamic getter
// Assertion testing list.spec.js
import listStore from "./list";
const {
mutations, getters
} = listStore;
it(
"Should add an item to the list",
() => {
const state = { list: [] };
const item = { foo: "bar" };
const expectedState = {
list: [{
foo: "bar"
}]
};
mutations
.addItem(state, item);
expect(state)
.toEqual(expectedState);
}
);
test input
expected output
code execution
assertion
// Assertion testing list.spec.js
import listStore from "./list";
const {
mutations, getters
} = listStore;
it(
"Should add an item to the list",
() => {
const state = { list: [] };
const item = { foo: "bar" };
const expectedState = {
list: [{
foo: "bar"
}]
};
mutations
.addItem(state, item);
expect(state)
.toEqual(expectedState);
}
);
it(
"Should get an item from the list",
() => {
const state = {
list: [{ foo: "bar" }]
};
const expectedItem = {
foo: "bar"
};
const item = getters
.getItem(state)(0);
expect(item)
.toEqual(expectedItem);
}
);
dynamic getter
test input
expected output
assertion
// A simple vuex store: list.js
const state = { list: [], status: "" };
const mutations = {
addItem, setStatus,
setItems, addItems
};
const actions = {
fetchItems: async ({ commit }, page) => {
try {
commit("setStatus", "loading");
const { data } = await axios
.get(`https://...?page=${page}`);
const action = page
? "setItems"
: "addItems";
commit(action, data);
commit("setStatus", "success");
} catch (error) {
console.error(error);
commit("setStatus", error.message);
}
}
};
export default { ... };
// Mock axios with jest
jest.mock("axios");
const actions = require("./list").actions;
it(
"Should add items to an empty list",
async () => {
const items = mockItems();
// Mock-resolve an axios request
axios.get
.mockResolvedValue({ data: items });
// Mock the commit function
const commit = jest.fn();
// Call fetchItems with page = 0
await actions.fetchItems({ commit }, 0);
expect(commit)
.toHaveBeenNthCalledWith(1, "setStatus", "loading");
expect(commit)
.toHaveBeenNthCalledWith(2, "setItems", items);
expect(commit)
.toHaveBeenNthCalledWith(3, "setStatus", "success");
}
);
it(
"Should add items to a populated list",
async () => { ... }
);
it(
"Should set an error status if the API errors",
async () => { ... }
);
A Wrapper is an object that contains a mounted component or vnode and methods to test the component or vnode.
Vue Test Utils tests Vue components by mounting them in isolation, mocking the necessary inputs ...
MyComponent.vue
Child.vue
Child.vue
Grandchild.vue
Grandchild.vue
mount(MyComponent)
Rendered JSDom Tree
MyComponent.vue
<child-stub />
<child-stub />
Rendered JSDom Tree
shallowMount(MyComponent)
Isolated component unit testing
... we recommend writing tests that assert your component's public interface ...
test input
assert output
{ shallowMount
(MyComponent) }
Contract
Non-contract
<template>
<div>
<span>
The first item is {{ getItem(0) }}
</span>
<button id="btn" @click="onClick">
Click Me!
</button>
<button
@click="addItem({ foo: 'bar' })"
>
Add Item
</button>
<pre>{{ secret }}</pre>
</div>
</template>
Contract
Non-contract
<script>
import Vue from "vue";
import { mapGetters, mapMutations } from "vuex";
export default Vue.extend({
props: {
secret: {
type: String,
required: false,
default: "No secret..."
}
},
computed: {
...mapGetters(["getItem"])
},
methods: {
...mapMutations(["addItem"]),
onClick() {
this.$emit("my-event");
}
}
});
</script>
Contract
Non-contract
import {
shallowMount, createLocalVue
} from "@vue/test-utils";
import Vuex from "vuex";
import list from "@/store/list";
import MyComponent from "./MyComponent.vue";
const localVue = createLocalVue();
localVue.use(Vuex);
const store = new Vuex.Store({
...list,
state: {
...list.state,
list: [{ foo: "bar" }]
}
});
it("renders props.secret when passed", () => {
const secret = "my secret message";
const wrapper = shallowMount(
MyComponent, {
propsData: { secret },
store,
localVue
}
);
expect(wrapper.find("pre").text())
.toMatch(secret);
});
test input
assertion
selector
it(
"emits 'my-event' if btn is clicked",
() => {
const wrapper = shallowMount(
MyComponent,
{
store,
localVue
}
);
const btn = wrapper.find("#btn");
btn.trigger("click");
expect(
wrapper.emitted()["my-event"].length
).toBe(1);
}
);
test input
assertion
it(
"prints the first item, two buttons, and 'secret'",
() => {
const wrapper = shallowMount(
MyComponent,
{
store,
localVue
}
);
expect(wrapper.element)
.toMatchSnapshot();
}
);
exports[`MyComponent.vue prints the first item, two buttons, and 'secret' 1`] = `
<div>
<span>
The first item is {
"foo": "bar"
}
</span>
<button
id="btn"
>
Click Me!
</button>
<button>
Add Item
</button>
<pre>
No secret...
</pre>
</div>
`;
// package.json
"scripts": {
...
"test:integration":
"vue-cli-service test:unit --testMatch \"**/src/**/*.int.(js)\""
}
/**
* @jest-environment node
*/
import listStore from "./list";
const { actions } = listStore;
describe(
"Integration of API with list store",
() => {
it("the API fetches items", async () => {
const commit = jest.fn();
await actions.fetchItems({ commit }, 0);
expect(commit).toHaveBeenCalledTimes(3);
});
}
);
assert commits
execute request
node environment to execute axios request
describe("MyComponent.vue", () => {
const secret = "my secret message";
const wrapper = mount(MyComponent, {
propsData: { secret },
store,
localVue
});
it(`
fetches and sets 10 items to the list,
and renders the span
`, async () => {
const fetchButton = wrapper.find("#fetch");
const items = mockItems();
// Mock-resolve an axios request
axios.get.mockResolvedValue({ data: items });
fetchButton.trigger("click");
await localVue.nextTick();
expect(store.state.list).toHaveLength(10);
});
});
assert mutated list's length
user interaction
describe("App.vue", () => {
const wrapper = mount(App, {
store,
localVue
});
it(`
sets a secret when 'btn' is clicked
`, () => {
const btn = wrapper.find("#btn");
btn.trigger("click");
expect(wrapper.vm.secret).not.toBe("");
});
});
assert updated data property
user interaction