Мой опыт тестирования в Ember

Андрей Михайлов (lolmaus)

Frotnend-разработчик в Perforce

 

https://lolma.us

https://github.com/lolmaus

https://perforce.com/products/helix-teamhub

Знакомство с методикой Cucumber
с помощью ember-cli-yadda

Пирамида потребностей

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

Acceptance

Integration

Unit

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

Acceptance

Integration

Unit

$$$

$

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

Acceptance

Integration

Unit

$$$

$

Правильная пирамида тестов

Unit

Integration

Acceptance

Controller.extend({
  spaceHorn: service(),
  society:   service(),

  caramelize () {
    const confusion = this.spaceHorn.honk()
    return this.society.riot(confusion)
  }
})

Цикл Red-Green-Refactor

Сначала пишем acceptance-тест

Пишем код,

чтобы тест

прошел

Рефакторим код

Цикл Red-Green-Refactor

Acceptance

тест

Код

Рефак-торинг

Unit

тест

Код

Рефак-торинг

Типичный acceptance test

test("apples should be sorted by id", function (assert) {
    const user = server.create("user")
    server.createList("apples", 20, {ownerId: user.id})

    authenticateSession(user)
    visit("/apples")
    
    const apples = findAll(".apples .apple")

    apples.forEach((currentApple, i) => {
        if (i === 0) return

        const currentValue = currentApple.querySelector(".apple-id").textContent
         
        const previousApple = apples[i - 1]
        const previousValue = previousApple.querySelector(".apple-id").textContent

        assert.ok(
            currentValue > previousValue,
            `Apple ${i} id should be greater than apple ${i -1} id`
        )
    })
})

Применяем page object

test("apples should be sorted by id", function (assert) {
    const user = server.create("user")
    server.createList("apples", 20, {ownerId: user.id})

    authenticateSession(user)
    page.visit()

    page.apples.forEach((currentApple, i) => {
        if (i === 0) return
         
        const previousApple = page.apples[i - 1]

        assert.ok(
            currentApple.id > previousApple.id,
            `Apple ${i} id should be greater than apple ${i -1} id`
        )
    })
})
export default create({
    visitable: "/apples",
    
    apples: collection({
        scope: ".apples",
        itemScope: ".apple",
        item: {
            id: text('.apple-id')
        }
    })
})

Выносим логику в отдельные функции

let user, apples

function createUser () {
    user = server.create("user")
}

function createApples () {
    apples = server.createList("apples", 20, {ownerId: user.id})
}

function login () {
    authenticateSession(user)
}

function visit () {
    page.visit()
}

function applesShouldBeSortedById (assert) {
    page.apples.forEach((currentApple, i) => {
        if (i === 0) return
         
        const previousApple = page.apples[i - 1]

        assert.ok(
            currentApple.id > previousApple.id,
            `Apple ${i} id should be greater than apple ${i -1} id`
        )
    })
}
test("apples should be sorted by id", function (assert) {
    createUser()
    createApples()

    login()
    visit()

    applesShouldBeSortedById(assert)
})

Переиспользуем функции

test("apples should be sorted by id (asc) by default", function (assert) {
    createUser()
    createApples()

    login()
    visit()

    applesShouldBeSortedBy({column: "id", order: "asc", assert})
})

test("apples should be sorted by id (desc) when URL contains order=desc", function (assert) {
    createUser()
    createApples()

    login()
    visit({order: "desc"})

    applesShouldBeSortedBy({column: "id", order: "desc", assert})
})

test("apples should be sorted by title (asc) when URL contains column=title", function (assert) {
    createUser()
    createApples()

    login()
    visit({column: "title"})

    applesShouldBeSortedBy({column: "title", order: "asc", assert})
})

test("apples should be sorted by title (desc) when URL contains column=title and order=desc", function (assert) {
    createUser()
    createApples()

    login()
    visit({column: "title", order: "desc"})

    applesShouldBeSortedBy({column: "title", order: "desc", assert})
})

DRY

const cases = [
  {queryParams: {                              }, column: "id",    order: "asc" },
  {queryParams: {                 order: "asc" }, column: "id",    order: "asc" },
  {queryParams: {                 order: "desc"}, column: "id",    order: "desc"},

  {queryParams: {column: "title"               }, column: "title", order: "asc" },
  {queryParams: {column: "title", order: "asc" }, column: "title", order: "asc" },
  {queryParams: {column: "title", order: "desc"}, column: "title", order: "desc"},

  {queryParams: {column: "color"               }, column: "color", order: "asc" },
  {queryParams: {column: "color", order: "asc" }, column: "color", order: "asc" },
  {queryParams: {column: "color", order: "desc"}, column: "color", order: "desc"},
]


cases.forEach(({queryParams, column, order}) => {
    const params = JSON.stringify(queryParams)

    test(`apples should be sorted by ${column} (${order}) when URL conains query params ${params}`, function (assert) {
        createUser()
        createApples()
    
        login()
        visit(queryParams)
    
        applesShouldBeSortedBy({column, order, assert})
    })
})

Cucumber, Gherkin, Yadda

Feature: Sorting apples

Scenario: Apples should be sorted by [Column] ([Order]) when URL contains QPs [QPs]
    Given a user
    And 20 apples

    When user is authenticated
    And user visits the apples page using QPs [QPs]

    Then apples should be sorted by [Column] column in [Order] order

Where:
    
    -----------------------------------------------------
    | QPs                              | Column | Order |
    | {}                               | id     | asc   |
    | {order: "asc"}                   | id     | asc   |
    | {order: "desc"}                  | id     | asc   |
    | {column: "title"}                | title  | asc   |
    | {column: "title", order: "asc"}  | title  | asc   |
    | {column: "title", order: "desc"} | title  | desc  |
    | {column: "color"}                | color  | asc   |
    | {column: "color", order: "asc"}  | color  | asc   |
    | {column: "color", order: "desc"} | color  | desc  |
    -----------------------------------------------------

    

Мой способ

определения steps

{
    "given a user" () {
        this.ctx.user = server.create("user")
    },

    "given $count apples" (countStr) {
        const count = parseInt(countStr, 10)
        const onwerId = this.ctx.user.id
        this.ctx.apples = server.createList("apple", count, {ownerId})
    },

    "when user is authenticated" () {
        authenticateSession(this.ctx.user)
    },

    "when user visits the apples page using QPs $qps" (qpsStr) {
        const qps = JSON.parse(qpsStr)
        page.visit(qps)
    },

    "then apples should be sorted by $column column in $order order" (column, order, assert) {
        page.apples.forEach((currentApple, i) => {
            if (i === 0) return
             
            const currentValue  = currentApple[column].text
            const previousApple = page.apples[i - 1]
            const previousValue = previousApple[column].text

            const result =
                order === "asc"  ? currentValue >= previousValue :
                order === "desc" ? currentValue <= previousValue :
                null
    
            assert.ok(result, `Apple ${i} id should be ${order} than apple ${i - 1} id`)
        })
    }
}

Позьза от Cucumber для программиста

  • Код тестов максимально легко читать, тесты не превращаются в балласт.
     
  • Код эффективно структурирован, максимальное переиспользование кода (DRY).
     
  • Переиспользование steps позволяет мгновенно создавать новые test cases.
     
  • Максимальная самодисциплина: сначала расписываем все возможные user stories, только после этого приступаем к программированию.
     
  • Работодатель счастлив.

Всем спасибо! ^_^

ember-cli-yadda

By Andrey Mikhaylov (lolmaus)