Если вы нашли и читаете данную статью, значит пришли к пониманию необходимости тестов в вашем приложении, а это залог качества написанного вами кода и забота о клиентах использующих ваши приложения.

Оглавление:

– зачем нужны тесты
– какие бывают тесты
– добавление тестов в проект
– структура созданного файла
– создаём объект тестирования
– перечень утверждений для тестов
– пишим тесты
– запускаем тестирование
– проверка покрытия кода тестами

И всё же, зачем нужны тесты?

Опишу самые, на мой взгляд, важные причины:

– при написании тестов вы продумываете все возможные варианты и ситуации, которые могут произайти в приложении, тем самым тестируете все эти подходы и исключаете возможные ошибки на «проде». Как простой пример: функция, выполняющая математическую операцию, должна выдавать определённый результат, но если посчитать руками, то вы обнаруживаете разницу, от которой можно обезопасится тестами 

– в момент написания нового функционала (либо вы вернулись к проекту через длительный промежуток времени) проще всего  запустить тесты и убедиться в том, что старый функционал по прежнему отрабатывает корректно

– ну и конечно, это экономия времени в будущем, особенно если ваш проект не маленький, а амбициозный и с долгосрочными планами

Какие бывают тесты?

Тесты подразделяются на три категории:

Unit test (Модульное тестирование) - это короткие тесты, которые проверяют на корректность работы конкретного куска кода (правильность выдаваемого результата)

Integration test (Интеграционное тестирование) - проверка взаимодействия разных компонентов приложения друг с другом. В том числе с внешними API.

UI test (Тестирование интерфейса) - имитация действий пользователя для проверки корректности реакции интерфейса должным образом.

Так как каждая из этих категорий довольно обширная для описания в одной статье, то здесь я затрону только Unit тесты.
P.S. Статьи по интеграционному тестированию и тестированию пользовательского интерфейса будут написаны следующими после этой.

Итак, приступим!

Добавление Unit тестов в проект

Как правило, при создании проекта, вы выбираете галочками необходимость добавления тестов и они (в виде шаблонов) создаются сами

Но в случае если вы этого не сделали в начале, и проект создан без тестов, то вызываем соответствующее меню через «File -> New -> Target…» где в открывшемся окне выбираем «Unit Test Bundle» и добавяляем его в проект

После этого вы увидите в навигаторе файлов проекта новую папку с файлом тестов (дефолтный шаблон)

Разбираем структуру файла с тестами

В первую очередь, при добавлении файла с тестами, необходимо указать ему таргет (цель) который мы будем тестировать.
Для этого необходимо добавить строку «@testable import UnitTestExample» (UnitTestExample - это название вашего проекта)

import XCTest
@testable import UnitTestExample

class UnitTestExampleTests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testExample() throws {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
        // Any test you write for XCTest can be annotated as throws and async.
        // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
        // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
    }

    func testPerformanceExample() throws {
        // This is an example of a performance test case.
        measure {
            // Put the code you want to measure the time of here.
        }
    }
}

setUp() - метод, в котором производится инициализация объектов для тестирования
tearDown() - метод, который по окончанию теста освобождает ресурты 

Следом за ними идут методы самого теста и теста производительности:

testExample() - метод в котором мы пишем код самого теста
testPerformanceExample() - метод который тестирует производительность размещённого в нём кода

Создаём объект тестирования

Перед тем как пускаться во все тяжкие начинать писать тесты, создадим объект тестирования. Им у нас будет выступать клас Coffee, который будет иметь свои свойства и методы (создадим отдельный файл):

class Coffee {
    // свойства, накопительный результат которых мы будем проверять на тестах
    var water = 0
    var milk = 0
    var groundCoffee = 0
    var sugar = 0
    var status = false
    
    // внутренний метод изготовления американо
    private func americano() {
        water += 1
        groundCoffee += 1
        status = true
    }
    
    // внутренний метод изготовления капучино
    private func cappuccino() {
        water += 1
        groundCoffee += 1
        milk += 1
        status = true
    }
    
    // метод запуска готовки выбранного варианта кофе с добавлением сахара (по желанию =))
    func makeCoffee(coffee: TypesOfCoffee, sugar: Int) {
        switch coffee {
        case .americano:
            americano()
        case .cappuccino:
            cappuccino()
        }
        
        self.sugar += sugar
    }
}

В созданном класе вы можете видеть несколько свойств, которые будут накапливать результат, а также две функции работающие только внутри класса и одна доступная снаружи для запуска изготовления кофе. (всё отметил в коде)

Перечень утверждений для тестов

Теперь, имея объект тестирования, можем приступить к написанию тестов! Но перед этим, для полного понимания возможных вариантов, опишу их ниже:

XCTAssert - утверждает, что выражение истинно
XCTAssertNil - утверждает, что выражение является nil
XCTAssertNotNil - утверждает, что выражение не является nil
XCTUnwrap - утверждает, что выражение не является Nil, и возвращает развернутое значение
XCTAssertEqual - утверждает, что два значения равны
XCTAssertNotEqual - утверждает, что два значения не равны
XCTAssertIdentical - утверждает, что два значения идентичны
XCTAssertNotIdentical - утверждает, что два значения не идентичны
XCTAssertGreaterThan - утверждает, что значение первого выражения больше значения второго выражения
XCTAssertGreaterThanOrEqual - утверждает, что значение первого выражения больше или равно значению второго выражения
XCTAssertLessThanOrEqual - утверждает, что значение первого выражения меньше или равно значению второго выражения
XCTAssertLessThan - утверждает, что значение первого выражения меньше значения второго выражения
XCTAssertThrowsError - утверждает, что выражение вызывает ошибку
XCTAssertNoThrow - утверждает, что выражение не вызывает ошибку
XCTFail - генерация немедленной остановки
XCTSkip - принудительный пропуск теста

Пишем тесты

В предыдущем разделе я описал существующие варианты утверждений в тестах, при помощи которых ваш тест приходит к тому или иному итогу. Давайте рассмотрим код ниже и разберём на премере некоторые из них.

import XCTest
@testable import UnitTestExample

class UnitTestExampleTests: XCTestCase {
    
    // создаём объект, который будем использовать в тестах
    var coffee: Coffee!

    override func setUpWithError() throws {
        try super.setUpWithError()
        // в данном методе, который запускается перед началом тестов, инициируем объект в виде класа, что позволит нам обращаться к его свойствам и методам
        coffee = Coffee()
    }

    override func tearDownWithError() throws {
        // убираем объект из памяти после окончания теста, освобождая память для запуска следующих тестов
        coffee = nil
        try super.tearDownWithError()
    }

    // метод самого теста
    func testExample() throws {
        
        let sugar = 2
        
        // "готовим" две кружки кофе, американо и капучино, каждая имеет различный состав ингридиентов, которые прописаны в соответствующих методах класа Coffee
        coffee.makeCoffee(coffee: .cappuccino, sugar: sugar)
        coffee.makeCoffee(coffee: .americano, sugar: 0)
        
        // проверяем статус готовности. тест пройдёт если утверждение будет true
        XCTAssert(coffee.status, "статус приготовления кофе")
        
        // проверяем корректность использования ингридиентов для готовки двух кружек кофе
        // на примере молока, сравниваем использованное количество молока с тем, которое реально должно было израсходоваться
        XCTAssertEqual(coffee.milk, 1, "добавление молока")
        XCTAssertEqual(coffee.water, 2, "добавление воды")
        XCTAssertEqual(coffee.groundCoffee, 2, "добавление молотого кофе")
        XCTAssertEqual(coffee.sugar, 2, "добавление сахара")

    }

    // метод тестирования скорости выполнения определённого блока кода
    func testPerformanceExample() throws {
        
        let sugar = 2
        
        measure {
            coffee.makeCoffee(coffee: .cappuccino, sugar: sugar)
        }
    }
}

** Прошу обратить внимание, что все методы тестов пишутся начинас с testНазваниеМетода , иначе они не будут восприниматься системой как тестовые

Комментарии оставлял в коде для большей наглядности.

Немного о тестировании производительности, а именно о методе testPerformanceExample():

Как вы уже наверно заметили, в этом методе появился блок measure поместив в который свой код, вы можете узнать его производительность.
Если вам не хватает стандартных показаний, то вы можете получить и вывести более детальную информацию следующим образом:

func testPerformanceExample() throws {
        
        let sugar = 2
        
        measure(
            metrics: [
              XCTClockMetric(),
              XCTCPUMetric(),
              XCTStorageMetric(),
              XCTMemoryMetric()
            ]
          ) {
            coffee.makeCoffee(coffee: .cappuccino, sugar: sugar)
        }
    }

XCTClockMetric() - показывает время за которое выполнился тест
XCTCPUMetric() - показывает взаимодействие с CPU (время, циклы, количество инструкций)
XCTStorageMetric() - измерение объёма данных, которые потенциально будут записаны в хранилище при выполнении указанного блока кода
XCTMemoryMetric() - показания использования оперативной памяти

Запускаем тестирование

Есть несколько вариантов запуска:

1. Через меню Product -> Test (что запкает все существующие тесты)
2. Через горячие клавиши Command + U (что так же запускает все существующие тесты)
3. Либо через навигационное меню, нажав на обозначенные ромбики, при помощи которых, вы можете запустить как все тесты разом, так и каждый по отдельности.

После успешного прохождения тестов все ромбики закрасятся зелёным.

Проверка покрытия кода тестами

Далее рассмотрим инструмент, при помощи которого вы можете детально отследить, на сколько полно вы протестировали код вашего приложения.

Для активации данного функционала, необходимо зайти в редактор схемы тестов

И установить галочку в пункте «Code Coverage»

После чего, запустив все тесты, к примеру через горячие клавиши Command + U и перейдя в отчёты в панели навигации мы увидим в подразделе проведённого теста % покрытия тестами каждого из файлов.

При необходимости вы можете провалиться в каждый из них.

Перейдя в файл и наведя на указанную область, она закрасится зелёным (если код покрывается тестами) или красным (если не покрывается).

Комментарии (1)


  1. YanSakhnevich
    09.03.2022 13:18

    Годный пост. Спасибо))