Share
Embed
LIFF Component Testing with Cypress
LINE Developers Thailand
September 13, 2020
Transcript
63 slides
-
11:15 - 11:50 Traitanit Huangsri LIFF Component Testing with Cypress
Senior Software Engineer, LINE Thailand -
About Me • Nott (Traitanit Huangsri) • Software Engineer -
LINE Thailand • Cypress Ambassador -
> Mobile & External Browsers Supported > New APIs: shareTargetPicker,
ready, getLineVersion etc. > npm install @line/liff // since LIFF 2.3 LIFF 2020 Brief Updates -
High Fidelity, Low Speed Low Fidelity, Very Fast UNIT TESTS
INTEGRATION TESTS E2E TESTS Automated Testing Strategy -
“It would be great if we have unit tests with
Higher FIDELITY and Good SPEED” -
Free & Open Source (MIT License) A tool to quickly
and reliably test anything that runs in a browser. -
All-in-one testing tool. Provides popular off-the-shelf Mocha, Sinon and Chai
and more bundled test utilities $ npm install -D cypress -
Your App & Your Tests Same Run Loop Provisioning &
Perform system-level tasks How Cypress Works ? -
describe('Demo', () => { it('is demo', () => { cy.get('[data-testid=input]').click().type('Hello
World'); cy.get('[data-testid=input]').should('have.text', 'Hello World'); }); }); Finding Elements Performing Actions Assertion Cypress API -
Not only End-to-End Tests But also Unit Tests With full
capabilities of Cypress Features -
LIFF SDK 1. Dependency with LINE Platform 2. Emulate Opening
LIFF from LINE App is so difficult 3. Negative Test ??? LIFF Testing Problems -
> Cypress automatically includes Sinon.js for stub & spy methods
> Expose it as Cypress.Sinon or cy.stub() and cy.spy() > Stub: a way to modify a function and control its behavior by your tests > Spy: Letting you capture and assert the function called with correct arguments Control LIFF SDK With Cypress -
sh$ vue add cypress-experimental # vue-cli > 3 > A
Cypress Test Helper to Unit Test Vue Components > Build on top of vue-test-utils > Runs natively on a real browser > Requires Cypress 4.5.0 or later, Node 8+ https://github.com/bahmutov/cypress-vue-unit-test Introducing Cypress Vue Unit Tests -
<template> <h1 data-testid="title">Hello {{ name }}!</h1> </template> <script> export default
{ name: 'Greetings', props: { name: { type: String, required: true } } } </script> Greetings.vue Basic Example -
Greetings.spec.js import { mount } from 'cypress-vue-unit-test' import Greetings from
'../../src/components/Greetings.vue' describe('Greetings', () => { it('Works awesomely', () => { const name = 'Cypress'; mount(Greetings, { propsData: { name, }, }); cy.get('[data-testid=title]').should('exist') .and('have.text', `Hello ${name}!`); }); }); Basic Example (TEST) -
App.vue created: async function () { try { await liff.init({
liffId: process.env.VUE_APP_LINE_LIFF_ID, }); } catch (error) { this.$swal({ icon: 'error', title: '<h2 data-testid="errorTitle">LIFF init failed</h2>', html: `<span data-testid="errorMsg">${error.message}</span>`, }); } } Scenario 1: Stub LIFF initialization LIFF Component Testing Examples -
App.spec.js describe('App', () => { it('liff init success', () =>
{ cy.stub(liff, 'init').as('liffInitStub').resolves(); mount(App); cy.get('[data-testid="init"]').should('be.visible') .and('have.text', 'Init success'); cy.get('@liffInitStub').should('be.calledWithExactly', { liffId: process.env.VUE_APP_LINE_LIFF_ID }); }); }); Scenario 1: Stub LIFF initialization LIFF Component Testing Examples -
App.spec.js describe('App', () => { it('liff init success', () =>
{ cy.stub(liff, 'init').as('liffInitStub').resolves(); mount(App); cy.get('[data-testid="init"]').should('be.visible') .and('have.text', 'Init success'); cy.get('@liffInitStub').should('be.calledWithExactly', { liffId: process.env.VUE_APP_LINE_LIFF_ID }); }); }); Scenario 1: Stub LIFF initialization LIFF Component Testing Examples -
App.spec.js describe('App', () => { it('liff init success', () =>
{ cy.stub(liff, 'init').as('liffInitStub').resolves(); mount(App); cy.get('[data-testid="init"]').should('be.visible') .and('have.text', 'Init success'); cy.get('@liffInitStub').should('be.calledWithExactly', { liffId: process.env.VUE_APP_LINE_LIFF_ID }); }); }); Scenario 1: Stub LIFF initialization LIFF Component Testing Examples -
App.spec.js describe('App', () => { it('liff init failure', () =>
{ const errorMessage = 'Init Error!'; cy.stub(liff, 'init').as('liffInitFailure').rejects(new Error(errorMessage)); mount(App); cy.get('[data-testid="errorTitle"').should('be.visible') .and('have.text', 'LIFF init failed'); cy.get('[data-testid="errorMsg"]').should('be.visible') .and('have.text', errorMessage); }); }); Scenario 2: LIFF Init Failure Negative Test with LIFF SDK -
Scenario 2: LIFF Init Failure Negative Test with LIFF SDK
App.spec.js describe('App', () => { it('liff init failure', () => { const errorMessage = 'Init Error!'; cy.stub(liff, 'init').as('liffInitFailure').rejects(new Error(errorMessage)); mount(App); cy.get('[data-testid="errorTitle"').should('be.visible') .and('have.text', 'LIFF init failed'); cy.get('[data-testid="errorMsg"]').should('be.visible') .and('have.text', errorMessage); }); }); -
async created() { await liff.ready; if (!liff.isLoggedIn()) { liff.login(); }
else { const profile = await liff.getProfile(); this.displayName = profile.displayName; this.pictureUrl = profile.pictureUrl; } }, Profile.vue Authentication with LINE Login -
describe('Profile', () => { beforeEach(() => { Cypress.sinon.replace(liff, 'ready', Promise.resolve());
}); it('render displayName when logged-in', () => { const profile = { displayName: 'Test User', pictureUrl: '<picturlUrl>' } cy.stub(liff, 'isLoggedIn').as('stubIsLoggedIn').returns(true); cy.stub(liff, 'getProfile').as('stubGetProfile').resolves(profile); mount(Profile).then(() => { // access everything in vue instance expect(Cypress.vue.$data.pictureUrl).to.equal(profile.pictureUrl); expect(Cypress.vue.$data.displayName).to.equal(profile.displayName); }); cy.get('[data-testid="pictureUrl"]').should('have.attr', 'src', profile.pictureUrl); cy.get('[data-testid="name"]').should('have.text', `Name: ${profile.displayName}`); }); }); Profile.spec.js Authentication with LINE Login -
describe('Profile', () => { beforeEach(() => { Cypress.sinon.replace(liff, 'ready', Promise.resolve());
}); it('render displayName when logged-in', () => { const profile = { displayName: 'Test User', pictureUrl: '<picturlUrl>' } cy.stub(liff, 'isLoggedIn').as('stubIsLoggedIn').returns(true); cy.stub(liff, 'getProfile').as('stubGetProfile').resolves(profile); mount(Profile).then(() => { // access everything in vue instance expect(Cypress.vue.$data.pictureUrl).to.equal(profile.pictureUrl); expect(Cypress.vue.$data.displayName).to.equal(profile.displayName); }); cy.get('[data-testid="pictureUrl"]').should('have.attr', 'src', profile.pictureUrl); cy.get('[data-testid="name"]').should('have.text', `Name: ${profile.displayName}`); }); }); Profile.spec.js Authentication with LINE Login -
describe('Profile', () => { beforeEach(() => { Cypress.sinon.replace(liff, 'ready', Promise.resolve());
}); it('render displayName when logged-in', () => { const profile = { displayName: 'Test User', pictureUrl: '<picturlUrl>' } cy.stub(liff, 'isLoggedIn').as('stubIsLoggedIn').returns(true); cy.stub(liff, 'getProfile').as('stubGetProfile').resolves(profile); mount(Profile).then(() => { // access everything in vue instance expect(Cypress.vue.$data.pictureUrl).to.equal(profile.pictureUrl); expect(Cypress.vue.$data.displayName).to.equal(profile.displayName); }); cy.get('[data-testid="pictureUrl"]').should('have.attr', 'src', profile.pictureUrl); cy.get('[data-testid="name"]').should('have.text', `Name: ${profile.displayName}`); }); }); Profile.spec.js Authentication with LINE Login -
Emulate Opening LIFF from LINE App export default { name:
'Mobile', data() { return { isInClient: false, os: '', } }, created() { this.isInClient = liff.isInClient(); this.os = liff.getOS(); } } -
Mobile.spec.js describe('Mobile', () => { it('emulates running app in LINE
App', () => { const ios = 'ios'; cy.stub(liff, 'isInClient').as('isInLineApp').returns(true); cy.stub(liff, 'getOS').as('os').returns(ios); cy.viewport(‘iphone-xr'); mount(Mobile).then(() => { expect(Cypress.vue.$data.isInClient).to.be.true; expect(Cypress.vue.$data.os).to.be.equal(ios); }); }); }); // emulate iPhone-XR resolution // assert Vue data property https://on.cypress.io/viewport Emulate Opening LIFF from LINE App -
Mobile.spec.js describe('Mobile', () => { it('emulates running app in LINE
App', () => { const ios = 'ios'; cy.stub(liff, 'isInClient').as('isInLineApp').returns(true); cy.stub(liff, 'getOS').as('os').returns(ios); cy.viewport(‘iphone-xr'); mount(Mobile).then(() => { expect(Cypress.vue.$data.isInClient).to.be.true; expect(Cypress.vue.$data.os).to.be.equal(ios); }); }); }); // emulate iPhone-XR resolution // assert Vue data property https://on.cypress.io/viewport Emulate Opening LIFF from LINE App -
Scenario 4: Emulate Opening LIFF From LINE App Emulate Opening
LIFF from LINE App -
sendMessages: async function () { if (liff.isLoggedIn()) { await liff.sendMessages([
{ type: 'text', text: 'Hello World', } ]); this.$swal({ icon: 'success', title: '<h2 data-testid="sent">Sent!</h2>', }); } } SendMessages.vue Integration with LINE Platform -
describe('Send Messages', () => { it('can send message with mock
function', () => { cy.stub(liff, 'sendMessages').as('sendMessages').resolves(); cy.stub(liff, 'isLoggedIn').as('isLoggedIn').returns(true); mount(SendMessages); cy.get('[data-testid="sendMessages"').click(); cy.get('[data-testid="sent"]').should('have.text', 'Sent!'); cy.get('@sendMessages').should('be.calledWithExactly', [ { type: 'text', text: 'Hello World', } ]); }); }); SendMessages.spec.js Integration with LINE Platform -
describe('Send Messages', () => { it('can send message with mock
function', () => { cy.stub(liff, 'sendMessages').as('sendMessages').resolves(); cy.stub(liff, 'isLoggedIn').as('isLoggedIn').returns(true); mount(SendMessages); cy.get('[data-testid="sendMessages"').click(); cy.get('[data-testid="sent"]').should('have.text', 'Sent!'); cy.get('@sendMessages').should('be.calledWithExactly', [ { type: 'text', text: 'Hello World', } ]); }); }); SendMessages.spec.js Integration with LINE Platform -
describe('Send Messages', () => { it('can send message with mock
function', () => { cy.stub(liff, 'sendMessages').as('sendMessages').resolves(); cy.stub(liff, 'isLoggedIn').as('isLoggedIn').returns(true); mount(SendMessages); cy.get('[data-testid="sendMessages"').click(); cy.get('[data-testid="sent"]').should('have.text', 'Sent!'); cy.get('@sendMessages').should('be.calledWithExactly', [ { type: 'text', text: 'Hello World', } ]); }); }); SendMessages.spec.js Integration with LINE Platform -
Feature vue-test-utils Cypress Running Test in Browser Time Travel Debugger
Debuggability Via Node Debugger Via Browser DevTools Reporting Text Based Cypress Dashboard + Screenshots + Video Recorded Test Fidelity ⬇ Very Low (virtual DOM) ⬆ Higher + All native browser APIs Interaction with Elements Limited API Use any Cypress Command Mock Library Jest Mock Sinon.js Code Coverage Comparison