Тестирование Фронтенда

Мысли вслух

Михаил Ангелов / @MikhailAngelov

Особенности фронтенда

  • содержат состояние
  • слабо контролируемая среда запуска
  • сильные внешние зависомости

Зачем тестировать

  • Быть уверенным что приложение работает как ожидается
  • Архитектура фронтенд-приложения достаточно хороша чтобы улучшать и расширять его

Пирамида тестов

Инструменты для тестирования фронтенда

Пример модульного теста (unit test).


const expect = require('chai').expect
const reducer = require('../../src/reducers/markers').default
const actions = require('../../src/actions')

describe('marker reducer',()=>{

    it('should ignore dummy action',()=>{
        const oldState = 'test'
        const state = reducer(oldState,{type:'BLA-BLA'})
        expect(state).to.equal(oldState)
    })

    it('should handle SEARCH action',()=>{
        const TERM = 'MERA'
        const action = actions.search(TERM)
        const state = reducer(undefined,action)

        expect(state.length).to.equal(1)
        expect(state[0].name).to.equal(TERM)
    })

    it('should handle TOGGLE_SIDEMENU action',()=>{
        const action = actions.toggleSideMenu()
        const state = reducer(undefined,action)

        expect(state.length > 10).to.equal(true)
    })
})
						

Пример компонентного теста (integration test).


const expect = require('chai').expect
const React = require('react')
const ReactTestUtils = require('react-addons-test-utils')
const SideMenu = require('../../src/components/sideMenu').default

describe('side menu', ()=>{
    
    it('should not display Term input if it closed',(done)=>{
        
        const component = ReactTestUtils.renderIntoDocument(
            // 〈SideMenu isOpen={false} onToggle={onClick} /> - does not work for stateless components
            SideMenu({isOpen:false, onToggle:onClick})
        );

        expect(ReactTestUtils.isCompositeComponent(component)).to.be.true
        const icon = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'searchIcon')
        ReactTestUtils.Simulate.click(icon)

        var inputs = ReactTestUtils.scryRenderedDOMComponentsWithClass(component, 'term')
        expect(inputs[0]).to.be.undefined

        function onClick(){
            expect(true).to.be.true
            done()
        }
    })
})
						

Пример компонентного теста (integration test).


const expect = require('chai').expect
const React = require('react')
const ReactTestUtils = require('react-addons-test-utils')
const Provider = require('react-redux').Provider
const configureMockStore = require('redux-mock-store')
const mockStore = configureMockStore()
const SideMenu = require('../../src/components/sideMenu').default
const SideMenuContainer = require('../../src/containers/sideMenuContainer').default

describe('side menu', ()=>{

    it('should display Term input if store has isOpen true',()=>{
        
        const store = mockStore({
            sideMenu: {isOpen:true}
        });
        const component = ReactTestUtils.renderIntoDocument(
            〈Provider store={store}>〈SideMenuContainer/>〈/Provider>
        );
        var input = ReactTestUtils.findRenderedDOMComponentWithClass(component, 'term')
        expect(!!input).to.be.true
    })
})
						

Пример теста полноценного (end-2-end test).


const webdriver = require('selenium-webdriver')

const driver = new webdriver.Builder().forBrowser('chrome').build()
const sideMenu = require('./pageObject/sideMenu')(driver)
const By = webdriver.By
const until = webdriver.until
const expect = require('chai').expect

it('should search stuff', function () {
    this.timeout(10000)

    beforeEach(()=> driver.navigate().to('http://localhost:3000'))
    after(()=>driver.quit())

    it('should search stuff', function () {

        sideMenu.isDisplayed()
        sideMenu.searchIcon().click()
        sideMenu.isOpen()
        sideMenu.searchFor('MERA')

        return sideMenu.resultItems().then(list=>{
            expect(list.length).to.equal(1)
            return list[0].getText().then(text=>{
                expect(text).to.equal('MERA')
            })
        })
    })
})
						

Страничный объект (page object).


const webdriver = require('selenium-webdriver')
const By = webdriver.By
const until = webdriver.until

module.exports = function(driver) {
    const elements = {
        searchIcon: By.css('.searchIcon'),
        searchText: By.css('.term'),
        resultList: By.css('.results'),
        resultItems: By.css('.resultItem'),
    };
    return {
        searchIcon: ()=>driver.findElement(elements.searchIcon),
        searchText: ()=>driver.findElement(elements.searchText),
        resultList: ()=>driver.findElement(elements.resultList),
        resultItems: ()=>driver.findElements(elements.resultItems),
        isDisplayed: ()=>driver.wait(until.elementLocated(elements.searchIcon)),
        isOpen: ()=>driver.wait(until.elementLocated(elements.searchText)),
        isClosed: ()=>driver.wait(until.elementIsNotVisible(elements.searchText)),
        searchFor: (value) => driver.findElement(elements.searchText).sendKeys(value)
    }
}
						

Более красивая версия с генераторами (end-2-end test).


const webdriver = require('selenium-webdriver')

const driver = require('./main')
const sideMenu = require('./pageObject/sideMenu')(driver)
const By = webdriver.By
const until = webdriver.until
const chai = require('chai')
const expect = chai.expect

it('should search stuff', function () {
    this.timeout(10000)
    
    beforeEach(()=> driver.navigate().to('http://localhost:3000'))
    after(()=>driver.quit())

    it('should search stuff', function* () {

        sideMenu.isDisplayed()
        sideMenu.searchIcon().click()
        sideMenu.isOpen()
        sideMenu.searchFor('MERA')

        expect(yield sideMenu.resultItems().then(list=>list.length)).to.equal(1)
        expect(yield sideMenu.resultItems().then(list=>list[0].getText())).to.equal('MERA')
    })
})
						

Спасибо за внимание

@MikhailAngelov

ссылка на презентацию