Эта серия статей основана на черновиках того, что я собираюсь в конечном итоге превратить в серию лекций и курсов для моих брокеров и сискрипторов. Обратная связь приветствуется, и если она окажется полезной, я буду рад указать вас в качестве соавтора.

СОДЕРЖАНИЕ

1. Что такое программа? - набор инструкций, выполняемых системой обработки информации.

2. Проблемная область - Как разработать программу / приложение

3. Хранение информации - Как моделировать информацию (данные) в системе обработки информации.

4. Логика и ошибки - два (основных) типа логики в системе обработки информации; как правильно обрабатывать ошибки

5. Разделение проблем - Самый важный принцип архитектуры программного обеспечения, с которым я когда-либо сталкивался.

6. Проверка программ с помощью тестов - объяснение теории, практики и преимуществ тестирования вашего программного обеспечения и применения разработки через тестирование.

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

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

Фактически, создание настройки и процесса тестирования, которые действительно работают для выбранной вами платформы или библиотеки, может занять несколько недель. Последний гвоздь в крышку гроба для большинства программистов заключается в том, что если вы не применяете разделение проблем в своих программах, что средний разработчик делает в лучшем случае плохо, тогда будет невозможно адекватно протестировать вашу программу. программа.

Поэтому, прежде чем вы продолжите, я скажу очень прямо: если программирование - это просто средство оплаты счетов для вас, и вы предпочитаете учиться и делать как можно меньше для достижения этой цели, то я не предлагаю вам беспокоиться. учимся тестировать свой код. Дело в том, что вам не нужно писать тесты, чтобы писать программы, которые будут приносить вам деньги. Я знаю это, потому что многие люди так поступали.

Если, с другой стороны, вы похожи на меня и ваша основная цель - написать наилучшие программы, которые вы можете написать, то тестирование - полезный и бесценный инструмент, который должен быть в вашем наборе инструментов. Как выясняется, ценность написания тестов напрямую связана со сложностью проблем, которые вы пытаетесь решить в коде, и со способностью применять управляемую тестами разработку в более сложных приложениях (как Я сделал это в SpaceNotes), позволит вам:

  • Решайте сложные проблемы быстрее, разбивая их на небольшие, четко определенные проблемы (я считаю, что быстрее пишу сложные программы, применяя TDD)
  • Выявите большинство своих ошибок до того, как вам когда-либо понадобится развернуть свою программу на устройстве (не верьте мне на слово, попробуйте и убедитесь сами)
  • Исправляйте ошибки быстрее, поскольку обычно вы сможете их изолировать (при условии, что вы правильно применили TDD)
  • Докажите свою программу способом, прямо аналогичным доказательству формулы математиком; который, когда все тесты пройдут, сообщит вам, что вы действительно выполнили свою программу

Почему люди ненавидят тестирование?

Инстинктивная (читай: начальная) реакция почти каждого программиста, с которым я разговаривал (я тоже чувствовал то же самое, поэтому не чувствую, что его осуждают) на тестирование и разработку через тестирование , был точно такой же ответ, как и у меня, когда в первый раз в классе математики мне сказали, что мне нужно «показать свою работу» при решении алгебраических уравнений. В конце концов, какой, черт возьми, смысл доказывать:

2х + 4 = 8; Решить относительно x

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

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

Не имеет значения, может ли это конкретное уравнение решить в уме. Всегда существует такая степень сложности, когда все шаги, необходимые для решения проблемы, невозможно удержать в уме в один момент; где возникает необходимость записывать каждый промежуточный шаг вниз. Как я надеюсь продемонстрировать, тот же принцип применим к TDD и тестированию в целом .

Как вы тестируете код?

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

Если вам когда-либо было интересно, что означает термин «Модульный тест», я лично думаю о модуле как о фрагменте кода неопределенной длины с единственной точкой входа (запись точка, как правило, является вызовом / вызовом одной функции; которая может быть верхнего уровня или частью класса / объекта / вещи). Общее определение Unit - это «наименьшая тестируемая часть приложения». Это определение имеет смысл в ретроспективе, но я не нашел его полезным, когда впервые учился тестировать. В любом случае не стоит слишком беспокоиться о словесных определениях; знать это в коде.

Предположим, мы хотим протестировать объект Калькулятор нашей простой программы из предыдущей статьи:

const val ERROR_MESSAGE = "An error has occured."
//'Thing' or 'Unit' to be tested:
class Calculator {
    fun evaluateExpression(binomialExpression: Expression): String {
        return when (binomialExpression.operatorSymbol) {
            "+" -> (binomialExpression.operandOne + binomialExpression.operandTwo).toString()
            "-" -> (binomialExpression.operandOne - binomialExpression.operandTwo).toString()
            "*" -> (binomialExpression.operandOne * binomialExpression.operandTwo).toString()
            "/" -> (binomialExpression.operandOne / binomialExpression.operandTwo).toString()
            else -> ERROR_MESSAGE
        }
    }
}
/* Just so there is no confusion, Expression is another 'Thing' which simply acts as a virtual representation of a binomial expression, by holding relevant data */
data class Expression(
        //A Double is a decimal number such as 1.245
        val operandOne: Double, 
        val operandTwo: Double,
        val operatorSymbol: String
)

Моя общая обработка для написания модульного теста с помощью TDD выглядит следующим образом:

  1. Напишите заглушку функции (пустую функцию) для теста
  2. Опишите на человеческом языке, что должен делать Блок, в комментарии над заглушкой функции.
  3. Если уместно, также опишите различные пути и результаты, которые могут возникнуть в модуле (это может быть необязательно для простых единиц, которые имеют только один ожидаемый результат).
  4. Подготовьте тестовые данные или взаимодействия, подходящие для тестового примера (здесь нам пригодится фиктивный фреймворк, о котором я расскажу позже)
  5. Вызов / вызов / выполнение объекта путем вызова функции его точки входа.
  6. Проверьте результирующее поведение и / или состояние данных, возвращаемых из модуля
  7. Выполните тестовую функцию и убедитесь, что она работает правильно (хотя на этом этапе она должна завершиться ошибкой, поскольку вы фактически не реализовали модуль).
  8. Реализовать (читать: писать) Единицу
  9. Выполнять тестовую функцию, пока она не пройдет; в противном случае повторите процесс с шага 8.

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

Более простой пример

Процесс, который я описал выше, - это то, как я буду писать модульные тесты с моими предпочтительными библиотеками; Я фанат библиотек модульного тестирования, таких как JUnit 5, и фреймворков для имитации, таких как Mockk или Mockito.

Прежде чем мы увидим этот процесс в коде, я хотел продемонстрировать тестирование в очень простой среде, свободной от библиотек. Это не то, что я обычно делаю, но это должно помочь вам понять принцип тестирования, если из-за вышеупомянутой бессвязности это звучит излишне сложно:

//fun main() is called when this program is first executed
fun main(args: Array<String>){
    testSuiteCalculator()
}

/**
 * Calculator solves a valid binomial expression.
 * Expression is comprised of:
 * operandOne - Number
 * operandTwo - Number
 * operatorType - One of String:
 * "+" add
 * "-" subtract
 * "/" divide
 * "*" multiply
 */
fun testSuiteCalculator(){
    //prepare tests with test data
    val addResult: String = evaluateTest(2.0, 2.0, "+")
    val subtractResult: String = evaluateTest(2.0, 2.0, "-")
    val divideResult: String = evaluateTest(2.0, 2.0, "/")
    val multiplyResult: String = evaluateTest(2.0, 2.0, "*")
    val invalidResult: String = evaluateTest(2.0, 2.0, "HueHueHue")

    //verify results
    if (addResult == "4.0") println("addTest Passed")
    else println("addTest Failed")

    if (subtractResult == "0.0") println("subtractTest Passed")
    else println("subtractTest Failed")

    if (divideResult == "1.0") println("divideTest Passed")
    else println("divideTest Failed")

    if (multiplyResult == "4.0") println("multiplyTest Passed")
    else println("multiplyTest Failed")

    if (invalidResult == ERROR_MESSAGE) println("invalidTest Passed")
    else println("invalidTest Failed")
}

//this would be a great case for Single Expression Syntax instead
fun evaluateTest(d1: Double, d2: Double, s: String): String {
    return Calculator.evaluateExpression(
            Expression(d1, d2, s)
    )
}

После выполнения fun main (…) консоль выведет:

addTest пройден
subtractTest пройден
divTest пройден
multiplyTest пройден
invalidTest пройден

Процесс завершен с кодом выхода 0

Если математик хочет доказать теорему Пифагора, он может подставить некоторые значения в теорему, а затем сравнить результат с измерениями, взятыми из реальных или нарисованных треугольников и квадратов. В нашем случае мы вставляем некоторые значения в модуль, который хотим протестировать, и настраиваем каким-либо образом тестирование результата, который дает нам модуль. . Эта форма тестирования довольно проста, но она отлично подходит для нашей простой программы.

Несколько примечаний:

  • Конечно, можно писать плохие тесты, поэтому вам нужно будет очень усердно писать сложные (реальная история: я действительно испортил одну из функций println () в приведенном выше коде, но вывод предупредил меня об ошибке)
  • Вам решать, сколько различных тестовых примеров вы хотите написать, но общее практическое правило - написать тестовый пример для каждого аргумента, который, как ожидается, приведет к уникальному поведению; включая случаи ошибок!
  • Если все ваши тесты составлены правильно (без опечаток), вы должным образом проверили все потенциальное поведение и результаты модуля и все ваши тесты прошли успешно, значит, вы доказали, что ваша программа теперь завершено

Реальная сделка

Я не буду вдаваться в подробности в этой статье о тестовой разработке в действии, так как я считаю, что это что-то лучше показано вживую (что я собираюсь сделать в ближайшее время ). Однако позвольте мне показать вам отрывки из того, как выглядят настоящие модульные тесты из SpaceNotes. Эти тесты были такого уровня сложности, что я очень выиграл от применения моего процесса TDD (и я так и сделал; это касается не только камеры). Я тщательно пометил и объяснил каждую строку для тех, кто не знаком с Kotlin - NoteDetailLogicTest .kt:

/**
 * When auth presses done, they are finished editing their note. They will be returned to a list
 * view of all notes. Depending on if the note isPrivate, and whether or not the user is
 * anonymous, will dictate where the note is written to.
 *
 * a. isPrivate: true, user: null
 * b. isPrivate: false, user: not null
 * c. isPrivate: true, user: not null
 *
 * 1. Check current user status: null (anonymous), isPrivate is beside the point if null user
 * 2. Create a copy of the note in vM, with updated "content" value
 * 3. exit to list activity upon completion
 */
@Test //Test is a JUnit 4/5 Annotation which allows an IDE like Android Studio to execute these tests on the JVM
//using backticks, I can give this function a legible English name
fun `On Done Click private, not logged in`() = runBlocking {
    //logic is the class I want to test which has the unit
    // see NoteDetailLogic.kt
    logic = getLogic()
    //every is a Mockk function which allows a mock (such as view)    to return a predefined response when logic calls its function(s)
    every {
        view.getNoteBody()
    //That which follows returns is the test data response (as discussed above)
    } returns getNote().contents

    every {
        vModel.getNoteState()
    } returns getNote()
//this is a special mock response function for "suspend functions (it is a Kotlin language feature)
    coEvery {
        anonymous.updateNote(getNote(), noteLocator, dispatcher)
    } returns Result.build { Unit }

    coEvery {
        auth.getCurrentUser(userLocator)
    } returns Result.build { null }

    //Call the Unit to be tested. NoteDetailEvent is a sealed class which represents a finite set of events which the logic class can receive [see NoteDetailContract.kt]
    logic.event(NoteDetailEvent.OnDoneClick)

    //verify confirms whether or not logic actually called the functions I want it to call during its execution. This is called behaviour verification

    verify { view.getNoteBody() }
    verify { vModel.getNoteState() }
    coVerify { auth.getCurrentUser(userLocator) }
    coVerify { anonymous.updateNote(getNote(), noteLocator, dispatcher) }
    verify { navigator.startListFeature() }
}

/**
 *b:
 * 1. get current value of noteBody
 * 2. write updated note to repositories
 * 3. exit to list activity
 */
//runBlocking is only necessary when testing suspending functions; otherwise you do not need it in Kotlin
@Test
fun `On Done Click private, logged in`() = runBlocking {
    logic = getLogic()

    every {
        view.getNoteBody()
    } returns getNote().contents

    every {
        vModel.getNoteState()
    } returns getNote()

    coEvery {
        registered.updateNote(getNote(), noteLocator)
    } returns Result.build { Unit }

    coEvery {
        auth.getCurrentUser(userLocator)
    } returns Result.build { getUser() }

    //call the unit to be tested
    logic.event(NoteDetailEvent.OnDoneClick)

    //verify interactions and state if necessary

    verify { view.getNoteBody() }
    verify { vModel.getNoteState() }
    coVerify { auth.getCurrentUser(userLocator) }
    coVerify { registered.updateNote(getNote(), noteLocator) }
    verify { navigator.startListFeature() }
}

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

Какую часть кода мне следует протестировать?

Я не могу ответить на этот вопрос, не вызывая у кого-то неизбежного раздражения, так как я видел очень сильные мнения по этой теме от старших разработчиков, которые не согласны (хотя я готов выслушать аргументы в комментариях). Мой общий ответ на этот вопрос состоит в том, что я помещаю вещи в некую иерархию важности:

  • Я пытаюсь протестировать любой класс, который координирует многие другие классы, такие как классы логики и взаимодействия в SpaceNotes (это также применимо к контроллерам и презентаторам в более широко известных архитектурах)
  • Я пытаюсь протестировать любой класс, в котором есть сложная логика

Некоторые люди утверждают, что вы всегда должны стремиться к 100% покрытию кода (тестировать буквально все), и я пока не могу сказать, будет ли это плодотворным усилием. Я часто использую шаблоны пассивное представление / скромный объект в своем коде, когда чувствую, что могу наблюдать за такими вещами; и я обнаружил, что до сих пор это работает хорошо. На момент написания этой статьи я еще не пытался добиться 100% покрытия кода, поэтому пока оставляю свое мнение. Обсуждение дешево; знать это в коде.

Поддержка и благодарность

Хотя я не обсуждал в этой статье ничего, что я не понимал в коде (если не указано иное), многие принципы разработки через тестирование были первоначально преподаны мне через работы Роберта Мартина . Пожалуйста, подумайте о том, чтобы проверить его содержание по этой теме; Я не верю, что вы будете разочарованы. Его книги тоже неплохие.

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

Присоединяйтесь к сообществу мудрыйАсс:
https://www.instagram.com/wiseassbrand/
https://www.facebook.com/wiseassblog/
https: // twitter.com/wiseass301
http://wiseassblog.com/
https://www.linkedin.com/in/ryan-kay-808388114

Поддержите мудрое сообщение здесь:
https://www.paypal.me/ryanmkay