Component testing based on browser rendering

Table of contents

Why Automated Testing is Needed

type of test

How to test components

white box testing

black box testing

gray box testing

recommended solution

Playwright Component Test Cases

Introduction to Playwright

playwright architecture diagram

BrowserContext

Principles of component testing

Component introduction

model package

Component rendering test

Component Props Test

Component Events Test

Component Slots Test

shortcut key test

Cypress Component Test Cases

Introduction to Cypress

start

Component introduction

model package

Component rendering test

Component Props Test

Component Events Test

Component Slots Test

shortcut key test

Summarize


Why Automated Testing is Needed

Automated testing is a method of using a computer to check whether the software is working properly. Once an automated test is created, it can be repeated countless times without any effort. Automated testing prevents unintentional introduction of bugs and encourages developers to decompose applications into testable and maintainable functions, modules, classes, and components.

In the process of manual testing, we should all have experienced or seen similar problems:

  • When the version is released, it takes several hours or even days to test our application, and the test of old functions accounts for a large proportion
  • As the functions of the application become more and more large and the number of participants increases, you will become more and more cautious when implementing a small feature or changing a BUG, ​​always worrying whether this will affect other functions
  • Code refactoring is always accompanied by a lot of regression testing

Through automated testing, you can effectively help the team improve these problems, allowing your team to build complex applications more quickly and confidently.

Note that not all applications have such characteristics and corresponding problems. For example, various activity pages on the C-side have a short application life cycle and a very short development cycle, so automation is completely unnecessary.

type of test

The following is more from the front-end perspective to classify our possible automated tests

  • Unit testing: Checks that inputs to a given function, class, or composite function produce expected output or side effects.
  • Component Testing: Check that your component mounts and renders properly, can be interacted with, and behaves as expected. These tests import more code, are more complex, and take more time to execute than unit tests.
  • UI test: Check whether the UI interface meets expectations, and often solve the dependency on the backend through Mock.
  • End-to-end testing: Checks functionality across multiple pages and makes actual network requests to production-built apps. These tests usually involve setting up a database or other backend. (UI testing in a broad sense can also be considered as end-to-end testing)

How to test components

white box testing

White box testing is aware of the implementation details and dependencies of a component. They are more focused on putting components into more independent tests. These tests usually involve mocking some subcomponents of some component, and setting the state and dependencies of the plugin.

Usually, the test implementations of various open source UI component libraries (such as ant-design ) are closer to the white box test type expressed here, and they generally use test frameworks such as Jest and Vitest.

black box testing

Black box testing does not know the implementation details of a component. These tests simulate as little as possible to test the real world of how the components work in the page. If your component contains multiple sub-components, for example, your business encapsulates a tree shuttle box TreeTransfer component, which contains a Tree component and a List component, then we will only choose the TreeTransfer component that integrates them Test instead of testing the Tree and List components individually.

gray box testing

Between the white box and the black box, it not only pays attention to the correctness of the input and output on the surface of the component, but also pays attention to the internal situation of the component. It is not as detailed and complete as white-box testing, but it pays more attention to internal logic than black-box testing, often judging the internal state through some representative phenomena, events, and signs.

The Components Testing function launched by Playwright and Cypress , which renders components in real browsers and then conducts automated tests on them, is a reflection closer to component black-box testing or gray-box testing.

recommended solution

We recommend using real browsers to render our components for black-box or gray-box testing. The closer the test is to the way the user uses it, the more credible it is. At the same time, testing frameworks such as Jest use jsdom to simulate and generate Dom, which also has many limitations. For example, some functional tests related to width and height calculations cannot be realized.

We choose the two most popular end-to-end testing frameworks to compare with the Components Testing function. They can render components in real browsers for component testing Playwright(41.8k Star).Cypress(40.4K)

Playwright Component Test Cases

Introduction to Playwright

Playwright is a framework launched by Microsoft in 2020 dedicated to web application testing and automation. They can test your web application running on Chromium, Firefox and WebKit browsers through the same set of APIs. Supports the use of Jascript/TypeScript/Java/Python/.NET languages.

For more features and introductions, please refer to the homepage of the official documentation .

Relationships and differences with Puppeteer (a library for manipulating browsers using NodeJS):

Its development team comes from Puppeteer, and Puppeteer has been widely welcomed. They decided to carry it forward and apply it to more browsers, and make full use of its advantages and pitfalls to make a lot of new designs. It also produced a lot of breaking changes, so there is Playwright.

playwright architecture diagram

 @playwright/test implements test case running, assertion, test report generation and other functions

The automated test code in @playwright/test will call various browser APIs in Playwright

And playwright uses Chrome DevTools Protocol to control the browser and uses Websocket to communicate, so there will be no frequent browser startup and communication establishment in the whole process

BrowserContext

It exists between browser and page. Each browser can create multiple completely independent contexts. The creation of context is fast and consumes less resources. Each context can create multiple pages.

Through this design, we can operate multiple session independent browser contexts, and at the same time, we can only start the browser once each time we run the test, and each test case uses an independent context for testing.

Principles of component testing

 start

Refer to How to get started in the official documentation

The component testing framework built by Playwright is also provided in the following two warehouses, and the complete code of all the demonstrations below can be found in it

Component introduction

// playwright/index.ts 在Vue2中引入组件
// Import styles, initialize component theme here.
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Vue.use(ElementUI);

// playwright/index.ts 在Vue3中引入组件
// Import styles, initialize component theme here.
import { beforeMount } from '@playwright/experimental-ct-vue/hooks';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

beforeMount(async ({ app }) => {
  app.use(ElementPlus);
});

In addition to the global import, you can also choose to import the corresponding tested components in the test file on demand.

model package

Similar to the page object model in end-to-end testing, we encapsulate the general behavior and logic in the component testing process to simplify logic and code reuse.

const useSelect = (ct: Locator, page: Page) => {
  const pickSelectOption = async ({ text, nth }: { text?: string; nth?: number }) => {
    if (text) {
      await page.locator(`.el-select-dropdown:visible .el-select-dropdown__item :text-is("${text}")`).click();
    } else {
      await page.locator(`.el-select-dropdown:visible .el-select-dropdown__item >> nth=${nth}`).click();
    }
  };

  const openPopover = async () => {
    await ct.locator('.el-input').click();
    // 等待popover动画执行完毕
    // eslint-disable-next-line playwright/no-wait-for-timeout
    await page.waitForTimeout(400);
  };

  return {
    pickSelectOption,
    openPopover,
  };
};

Component rendering test

We recommend using visual comparisons to test whether components render as expected.

 
 
<!-- Select.vue -->
<template>
  <el-select v-bind="propsParams" v-model="value" placeholder="请选择" v-on="eventsParams">
    <el-option
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="item.value"
      :disabled="item.disabled"
    >
    </el-option>
  </el-select>
</template>

<script>
export default {
  props: {
    // component props
    propsParams: {
      type: Object,
      default: () => ({}),
    },
    // component events
    eventsParams: {
      type: Object,
      default: () => ({}),
    },
    // custom props
    defaultValue: {
      type: String,
      default: '',
    },
    options: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      value: this.defaultValue,
    };
  },
};
</script>

test('mount work', async ({ page, mount }) => {
  const ct = await mount(SelectBase, {
    props: {
      // custom props
      options: baseOptions,
    },
  });
  const { openPopover } = useSelect(ct, page);

  await openPopover();

  // Visual comparisons
  // allow 5% pixe ratio diff
  await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.5 });
});

Component Props Test

test('single select work', async ({ page, mount }) => {
  const ct = await mount(SelectBase, {
    props: {
      options: baseOptions,
      defaultValue: baseOptions[0].value,
    },
  });
  const { pickSelectOption, openPopover } = useSelect(ct, page);

  await openPopover();
  await pickSelectOption({ text: baseOptions[1].label });

  await expect(ct.locator('.el-input input')).toHaveValue(baseOptions[1].label);
});

Component Events Test

test('event work', async ({ page, mount }) => {
  const messages: string[] = [];

  const ct = await mount(SelectBase, {
    props: {
      propsParams: {
        clearable: true,
      },
      eventsParams: {
        change: () => messages.push('change-trigger'),
        clear: () => messages.push('clear-trigger'),
        'visible-change': () => messages.push('visible-change-trigger'),
      },
      options: baseOptions,
    },
  });
  const { pickSelectOption, openPopover } = useSelect(ct, page);

  await openPopover();
  await pickSelectOption({ text: baseOptions[0].label });

  await ct.locator('.el-input').hover();
  await ct.locator('.el-icon-circle-close').click();

  expect(messages).toContain('change-trigger');
  expect(messages).toContain('clear-trigger');
  expect(messages).toContain('visible-change-trigger');
});

Component Slots Test

test('slots work', async ({ mount }) => {
  const ct = await mount(Button, {
    slots: {
      default: 'click me',
    },
  });

  await expect(ct).toContainText('click me');
});

test('jsx slots work', async ({ mount }) => {
  const ct = await mount(<el-button>click me</el-button>);

  await expect(ct).toContainText('click me');
});

shortcut key test

test('keyboard operations', async ({ page, mount }) => {
  const ct = await mount(SelectBase, {
    props: {
      options: baseOptions,
      defaultValue: baseOptions[0].value,
    },
  });
  const { openPopover } = useSelect(ct, page);

  await openPopover();

  await ct.locator('.el-input').press('ArrowDown');
  await ct.locator('.el-input').press('ArrowDown');
  await ct.locator('.el-input').press('Enter');

  await expect(ct.locator('.el-input input')).toHaveValue(baseOptions[1].label);
});

Cypress Component Test Cases

Introduction to Cypress

Cypress is a JavsScript-based front-end testing tool that can perform faster, simple and reliable testing of any content running in the browser. It can be used to write all types of tests (end-to-end tests, interface tests, unit tests).

More features and introductions can be found in the Features chapter of Why Cyrpess .

Cypress architecture diagram

 The test code of Cypress and the web application under test will run directly in the same browser without the need for additional drivers (such as WebDriwer). Cypress has strong control over the web application under test.

At the browser level, Cypress can directly operate Dom, Window, Local Storage, network and other content from web applications.

Behind the browser is a Nodejs process. After the browser is started through it, the WebSocket link will be used to communicate with the browser. At the same time, proxy control will be performed on network requests here, and operations such as reading and changing network requests can be done.

At the operating system level, Cypress can perform operations such as screenshots, video recording, and file reading and writing through the NodeJs process.

start

Refer to Quick Start vue in the official documentation

The component testing framework built by Cypress is also provided in the following warehouse, and the complete code of all the demos below can be found in it

Component introduction

// cypress/support/component.ts 在Vue3引入全局组件,引入方式不友好,Vue3里面更推荐按需引入
import { mount } from 'cypress/vue'
import Button from '../../src/components/Button.vue'

Cypress.Commands.add('mount', (component, options = {}) => {
  // Setup options object
  options.global = options.global || {}
  options.global.components = options.global.components || {}

  // Register global components
  options.global.components['Button'] = Button

  return mount(component, options)
})

// cypress/support/component.ts 在Vue2引入全局组件
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import { mount } from 'cypress/vue';

Vue.use(ElementUI);

Cypress.Commands.add('mount', mount);

model package

const selectModal = {
  pickSelectOption: ({ text, nth }: { text?: string; nth?: number }) => {
    if (text) {
      cy.get('.el-select-dropdown:visible .el-select-dropdown__item').contains(text).click();
    } else {
      cy.get(`.el-select-dropdown:visible .el-select-dropdown__item::nth-child(${nth})`).click();
    }
  },
  openPopover: () => {
    // 等待popover动画执行完毕
    cy.get('.el-input').click().wait(400);
  },
};

Component rendering test

<template>
  <el-select v-bind="propsParams" v-model="value" placeholder="请选择" v-on="eventsParams">
    <el-option
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="item.value"
      :disabled="item.disabled"
    >
    </el-option>
  </el-select>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import { ElSelect, ElOption } from 'element-plus';

interface OptionItem {
  value: string;
  label: string;
  disabled?: boolean;
}

const props = withDefaults(
  defineProps<{
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    propsParams?: Record<string, any>;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    eventsParams?: Record<string, any>;
    defaultValue?: string;
    options: OptionItem[];
  }>(),
  {
    propsParams: () => ({}),
    eventsParams: () => ({}),
    defaultValue: '',
    options: () => [],
  },
);

const value = ref(props.defaultValue);
</script>

it('mount work', () => {
  cy.mount(SelectBase, {
    props: {
      options: baseOptions,
    },
  });

  selectModal.openPopover();

  /**
   * 官方文档推荐的cypress-plugin-snapshots插件在cypress10.6.0使用时报错
   * 相关issue见:https://github.com/meinaart/cypress-plugin-snapshots/issues/215
   * 这里使用https://github.com/FRSOURCE/cypress-plugin-visual-regression-diff 来实现视觉对比
   */
  cy.matchImage();
});

Component Props Test

it('single select work', () => {
  cy.mount(SelectBase, {
    props: {
      options: baseOptions,
      defaultValue: baseOptions[0].value,
    },
  });

  selectModal.openPopover();
  selectModal.pickSelectOption({ text: baseOptions[1].label });
  cy.get('.el-input input').should('have.value', baseOptions[1].label);

Component Events Test

it('event work', () => {
  const messages: string[] = [];

  cy.mount(SelectBase, {
    props: {
      propsParams: {
        clearable: true,
      },
      eventsParams: {
        change: () => messages.push('change-trigger'),
        clear: () => messages.push('clear-trigger'),
        'visible-change': () => messages.push('visible-change-trigger'),
      },
      options: baseOptions,
    },
  });

  selectModal.openPopover();
  selectModal.pickSelectOption({ text: baseOptions[0].label });

  cy.get('.el-input').click();
  cy.get('.el-select__icon:visible').click();

  cy.wrap(messages)
    .should('include', 'clear-trigger')
    .should('include', 'change-trigger')
    .should('include', 'visible-change-trigger');
});

Component Slots Test

it('slot work', () => {
  cy.mount(ElButton, {
    slots: {
      default: () => <span>click me</span>,
    },
  });

  cy.get('button').should('have.text', 'click me');
});

it('jsx slot work', () => {
  cy.mount(() => <ElButton>click me</ElButton>);

  cy.get('button').should('have.text', 'click me');
});

shortcut key test

it('keyboard operations work', () => {
  cy.mount(SelectBase, {
    props: {
      options: baseOptions,
      defaultValue: baseOptions[0].value,
    },
  });

  selectModal.openPopover();

  cy.get('.el-input').type('{downArrow}');
  cy.get('.el-input').type('{downArrow}');
  cy.get('.el-input').type('{enter}');

  cy.get('.el-input input').should('have.value', baseOptions[1].label);
});

Summarize

In front-end automated testing, component testing is the most cost-effective type of testing that we consider. When testing components, there is basically no dependence on other services and can be tested independently. At the same time, various basic components and business components are also widely used in our For various places in the system, ensuring their quality through automated testing is of great help to the improvement of the overall quality of the system.

In terms of component testing, there is basically no functional gap between the two testing frameworks. If you only consider component testing, then these two frameworks are highly recommended.



This post ends here, and finally, I hope that friends who read this post can gain something.

 How to get it: Leave a message [777] to get it for free

If you think the article is not bad, please like, share, and leave a message , because this will be the strongest motivation for me to continue to output more high-quality articles!

Guess you like

Origin blog.csdn.net/weixin_67553250/article/details/131250309