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 full-size slide

  2. LIFF Component
    Testing with
    Cypress

    View full-size slide

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

    View full-size slide

  4. LIFF 2020 Updates

    View full-size slide

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

    View full-size slide

  6. LET’S TALK ABOUT TESTING

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. LET ME Introduce

    View full-size slide

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

    View full-size slide

  14. Stateless Stateful
    Why Cypress ?

    View full-size slide

  15. $ npm install -D cypress

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  18. Web Frameworks Compatibility

    View full-size slide

  19. Command Log
    Your App
    Cypress UI Test Runner

    View full-size slide

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

    View full-size slide

  21. Automatic Wait

    View full-size slide

  22. Realtime Reloads

    View full-size slide

  23. 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 full-size slide

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

    View full-size slide

  25. LIFF Component Testing with

    View full-size slide

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

    View full-size slide

  27. LIFF SDK
    LIFF Testing Problems

    View full-size slide

  28. > 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 full-size slide

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

    View full-size slide

  30. 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 full-size slide


  31. 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 full-size slide

  32. 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 full-size slide

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

    View full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

  36. 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 full-size 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 full-size slide

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

    View full-size slide

  39. 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 full-size slide

  40. 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 full-size slide

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

    View full-size slide

  42. 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 full-size slide

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

    View full-size slide

  44. 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 full-size slide

  45. 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 full-size 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 full-size slide

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

    View full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

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

    View full-size slide

  52. 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 full-size slide

  53. 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 full-size slide

  54. 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 full-size 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 full-size slide

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

    View full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

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

    View full-size slide

  60. Blog Available Today!

    View full-size slide