Upgrade to Pro — share decks privately, control downloads, hide ads and more …

LIFF Component Testing with Cypress

LIFF Component Testing with Cypress

LINE Developers Thailand

September 13, 2020
Tweet

More Decks by LINE Developers Thailand

Other Decks in Technology

Transcript

  1. 11:15 - 11:50
    Traitanit Huangsri
    LIFF Component
    Testing with
    Cypress
    Senior Software Engineer, LINE Thailand

    View Slide

  2. View Slide

  3. LIFF Component
    Testing with
    Cypress

    View Slide

  4. About Me
    • Nott (Traitanit Huangsri)
    • Software Engineer - LINE Thailand
    • Cypress Ambassador

    View Slide

  5. LIFF 2020 Updates

    View Slide

  6. > Mobile & External Browsers Supported
    > New APIs: shareTargetPicker, ready, getLineVersion etc.
    > npm install @line/liff // since LIFF 2.3
    LIFF 2020 Brief Updates

    View Slide

  7. LET’S TALK ABOUT TESTING

    View Slide

  8. Let’s find the answer together!
    How Do You Test Your LIFF App ?

    View Slide

  9. UNIT TESTS
    INTEGRATION
    TESTS
    E2E TESTS
    Testing Pyramid
    Automated Testing Strategy

    View Slide

  10. Testing Pyramid
    UNIT TESTS
    INTEGRATION
    TESTS
    E2E TESTS
    Automated Testing Strategy

    View Slide

  11. High Fidelity, Low Speed
    Low Fidelity, Very Fast
    UNIT TESTS
    INTEGRATION
    TESTS
    E2E TESTS
    Automated Testing Strategy

    View Slide

  12. “It would be great if we have
    unit tests with
    Higher FIDELITY and Good SPEED”

    View Slide

  13. LET ME Introduce

    View Slide

  14. Free & Open Source (MIT License)
    A tool to quickly and reliably test
    anything that runs in a browser.

    View Slide

  15. Stateless Stateful
    Why Cypress ?

    View Slide

  16. $ npm install -D cypress

    View Slide

  17. All-in-one testing tool.
    Provides popular
    off-the-shelf Mocha, Sinon and
    Chai and more bundled test utilities
    $ npm install -D cypress

    View Slide

  18. Your App
    & Your Tests
    Same Run Loop
    Provisioning &
    Perform system-level
    tasks
    How Cypress Works ?

    View Slide

  19. Web Frameworks Compatibility

    View Slide

  20. Command Log
    Your App
    Cypress UI Test Runner

    View Slide

  21. Trace Error
    to your source
    Human Readable
    Error Messages
    Debuggability

    View Slide

  22. Time Travel

    View Slide

  23. Automatic Wait

    View Slide

  24. Realtime Reloads

    View Slide

  25. 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

    View Slide

  26. Not only End-to-End Tests
    But also Unit Tests
    With full capabilities of Cypress Features

    View Slide

  27. LIFF Component Testing with

    View Slide

  28. LIFF SDK
    1. Dependency with LINE Platform
    2. Emulate Opening LIFF from LINE App is so difficult
    3. Negative Test ???
    LIFF Testing Problems

    View Slide

  29. LIFF SDK
    LIFF Testing Problems

    View Slide

  30. > 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

    View Slide

  31. Vue.js Cypress
    LIFF
    Let’s Get Started

    View Slide

  32. 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

    View Slide


  33. Hello {{ name }}!

    <br/>export default {<br/>name: 'Greetings',<br/>props: {<br/>name: {<br/>type: String,<br/>required: true<br/>}<br/>}<br/>}<br/>
    Greetings.vue
    Basic Example

    View Slide

  34. 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)

    View Slide

  35. $ vue-cli-service test:component
    Component Test with Cypress

    View Slide

  36. App.vue
    created: async function () {
    try {
    await liff.init({
    liffId: process.env.VUE_APP_LINE_LIFF_ID,
    });
    } catch (error) {
    this.$swal({
    icon: 'error',
    title: 'LIFF init failed',
    html: `${error.message}`,
    });
    }
    }
    Scenario 1: Stub LIFF initialization
    LIFF Component Testing Examples

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

  39. 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

    View Slide

  40. Display stub was called
    Scenario 1: Stub LIFF initialization
    Running Component Tests

    View Slide

  41. 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

    View Slide

  42. 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);
    });
    });

    View Slide

  43. Scenario 2: LIFF Init Failure
    Negative Test with LIFF SDK

    View Slide

  44. 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

    View Slide

  45. Scenario 3: Test Login with LINE
    Authentication with LINE Login

    View Slide

  46. describe('Profile', () => {
    beforeEach(() => {
    Cypress.sinon.replace(liff, 'ready', Promise.resolve());
    });
    it('render displayName when logged-in', () => {
    const profile = {
    displayName: 'Test User',
    pictureUrl: ''
    }
    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

    View Slide

  47. describe('Profile', () => {
    beforeEach(() => {
    Cypress.sinon.replace(liff, 'ready', Promise.resolve());
    });
    it('render displayName when logged-in', () => {
    const profile = {
    displayName: 'Test User',
    pictureUrl: ''
    }
    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

    View Slide

  48. describe('Profile', () => {
    beforeEach(() => {
    Cypress.sinon.replace(liff, 'ready', Promise.resolve());
    });
    it('render displayName when logged-in', () => {
    const profile = {
    displayName: 'Test User',
    pictureUrl: ''
    }
    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

    View Slide

  49. Scenario 3: Test Login with LINE
    Authentication with LINE Login

    View Slide

  50. Emulate Opening LIFF
    from LINE App
    export default {
    name: 'Mobile',
    data() {
    return {
    isInClient: false,
    os: '',
    }
    },
    created() {
    this.isInClient = liff.isInClient();
    this.os = liff.getOS();
    }
    }

    View Slide

  51. 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

    View Slide

  52. 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

    View Slide

  53. Scenario 4: Emulate Opening LIFF From LINE App
    Emulate Opening LIFF from LINE App

    View Slide

  54. sendMessages: async function () {
    if (liff.isLoggedIn()) {
    await liff.sendMessages([
    {
    type: 'text',
    text: 'Hello World',
    }
    ]);
    this.$swal({
    icon: 'success',
    title: 'Sent!',
    });
    }
    }
    SendMessages.vue
    Integration with LINE Platform

    View Slide

  55. 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

    View Slide

  56. 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

    View Slide

  57. 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

    View Slide

  58. Scenario 5: Send Message to Chat
    Integration with LINE Platform

    View Slide

  59. 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

    View Slide

  60. Cypress React Unit Test
    https://github.com/bahmutov/cypress-react-unit-test
    Cypress Angularjs Unit Test
    https://github.com/bahmutov/cypress-angular-unit-test
    Not a Vue.Js Developer?

    View Slide

  61. https://fb.com/groups/cypressiothailand
    Join us Cypress.io Thailand

    View Slide

  62. Blog Available Today!

    View Slide

  63. View Slide