Эта серия статей основана на черновиках того, что я собираюсь в конечном итоге превратить в серию лекций и курсов для моих брокеров и сискрипторов. Обратная связь приветствуется, и если она окажется полезной, я буду рад указать вас в качестве соавтора.
СОДЕРЖАНИЕ
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 выглядит следующим образом:
- Напишите заглушку функции (пустую функцию) для теста
- Опишите на человеческом языке, что должен делать Блок, в комментарии над заглушкой функции.
- Если уместно, также опишите различные пути и результаты, которые могут возникнуть в модуле (это может быть необязательно для простых единиц, которые имеют только один ожидаемый результат).
- Подготовьте тестовые данные или взаимодействия, подходящие для тестового примера (здесь нам пригодится фиктивный фреймворк, о котором я расскажу позже)
- Вызов / вызов / выполнение объекта путем вызова функции его точки входа.
- Проверьте результирующее поведение и / или состояние данных, возвращаемых из модуля
- Выполните тестовую функцию и убедитесь, что она работает правильно (хотя на этом этапе она должна завершиться ошибкой, поскольку вы фактически не реализовали модуль).
- Реализовать (читать: писать) Единицу
- Выполнять тестовую функцию, пока она не пройдет; в противном случае повторите процесс с шага 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