Как мне протестировать систему, в которой объекты трудно подделать?

34

Я работаю со следующей системой:

Network Data Feed -> Third Party Nio Library -> My Objects via adapter pattern

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

Проблема:

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

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

Вопрос:

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

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

durron597
источник
3
Может ли ваша «записать» реальный поток данных и «воспроизвести» его позже в сторонней библиотеке?
Идан Арье
2
Кто-то может написать книгу о таких проблемах. На самом деле Майкл Фезерс написал именно эту книгу: c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCode В ней он описывает ряд методов для устранения сложных зависимостей, чтобы код мог стать более тестируемым.
cbojar
2
Адаптер вокруг сторонней библиотеки? Да, это именно то, что я рекомендую. Эти юнит-тесты не улучшат ваш код. Они не сделают его более надежным или более ремонтопригодным. Вы просто частично дублируете чужой код в этот момент; в этом случае вы дублируете какой-то плохо написанный код из звука. Это чистый убыток. В некоторых ответах предлагается провести интеграционное тестирование; Это хорошая идея, если вы просто хотите, "Это работает?" санитарная проверка. Хорошее тестирование - это сложная задача, требующая столько же навыков и интуиции, сколько и хороший код.
jpmc26
4
Прекрасная иллюстрация зла встроенных модулей. Почему не библиотека возвращает Timestampкласс (содержащий любое представление , что они хотят) и обеспечивает названные методы ( .seconds(), .milliseconds(), .microseconds(), .nanoseconds()) и, конечно же, названные конструкторы. Тогда бы не было проблем.
Матье М.
2
Здесь приходит на ум поговорка «все проблемы в кодировании могут быть решены с помощью уровня косвенности (кроме, конечно, проблемы слишком большого количества уровней косвенности)» ..
Дэн Пантри

Ответы:

27

Похоже, вы уже делаете должную осмотрительность. Но ...

На самом практическом уровне всегда включайте в свой набор кучу тестов интеграции с «полным циклом» для своего собственного кода и пишите больше утверждений, чем вы думаете. В частности, у вас должно быть несколько тестов, которые выполняют полный цикл create-read- [do_stuff] -validate.

[TestMethod]
public void MyFormatter_FormatsTimesCorrectly() {

  // this test isn't necessarily about the stream or the external interpreter.
  // but ... we depend on them working how we think they work:
  var stream = new StreamThingy();
  var interpreter = new InterpreterThingy(stream);
  stream.Write("id-123, some description, 12345");

  // this is what you're actually testing. but, it'll also hiccup
  // if your 3rd party dependencies introduce a breaking change.
  var formatter = new MyFormatter(interpreter);
  var line = formatter.getLine();
  Assert.equal(
    "some description took 123.45 seconds to complete (id-123)", line
  );
}

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

Предположим, вы должны понимать и зависеть от того, как анализатор JSON интерпретирует каждый «тип» в строке JSON. Полезно и тривиально включить что-то подобное в ваш набор:

[TestMethod]
public void JSONParser_InterpretsTypesAsExpected() {
  String datastream = "{nbr:11,str:"22",nll:null,udf:undefined}";
  var o = (new JSONParser()).parse(datastream);

  Assert.equal(11, o.nbr);
  Assert.equal(Int32.getType(), o.nbr.getType());
  Assert.equal("22", o.str);
  Assert.equal(null, o.nll);
  Assert.equal(Object.getType(), o.nll.getType());
  Assert.isFalse(o.KeyExists(udf));
}

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

И в значительной степени это просто нормально.

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

svidgen
источник
Тьфу, хотелось бы, чтобы это было так же просто, как вставить строку json в библиотеку. Это не. Я не могу сделать эквивалент (new JSONParser()).parse(datastream), так как они получают данные непосредственно из a, NetworkInterfaceи все классы, которые выполняют фактический анализ, являются частными пакетами и защищены.
durron597
Кроме того, журнал изменений не включал тот факт, что они изменили метки времени с ms на ns, среди других головных болей, которые они не документировали. Да, я очень недоволен ими, и я выразил им это.
durron597
@ durron597 О, почти никогда не бывает. Но вы часто можете подделать базовый источник данных - как в первом примере кода. ... Дело в том, что по возможности делайте интеграционные тесты с полным циклом, проверяйте свое понимание библиотеки, когда это возможно, и просто знайте, что вы все равно будете пускать ошибки в дикую природу. А ваши сторонние поставщики должны нести ответственность за внесение невидимых, разрушающих изменений.
svidgen
@ durron597 Я не знаком с NetworkInterface... это то, что вы можете передавать данные, подключив интерфейс к порту на localhost или что-то еще?
svidgen
NetworkInterface, Это низкоуровневый объект для непосредственной работы с сетевой картой, открытия сокетов и т. Д.
durron597
11

Краткий ответ: это сложно. Вы, вероятно, чувствуете, что нет хороших ответов, и это потому, что нет простых ответов.

Длинный ответ: Как говорит @ptyx , вам нужны системные тесты и интеграционные тесты, а также модульные тесты:

  • Модульные тесты быстры и просты в запуске. Они отлавливают ошибки в отдельных разделах кода и используют макеты, чтобы сделать возможным их запуск. По необходимости они не могут отлавливать несоответствия между фрагментами кода (например, миллисекунды или наносекунды).
  • Интеграционные тесты и системные тесты медленны (er) и трудны (er) для запуска, но обнаруживают больше ошибок.

Некоторые конкретные предложения:

  • Есть некоторая выгода от простого запуска системного теста для запуска как можно большей части системы. Даже если это не может подтвердить очень много поведения или очень хорошо в определении проблемы. (Micheal Feathers обсуждает это больше в Эффективной работе с Устаревшим Кодексом .)
  • Инвестирование в тестируемость помогает. Здесь можно использовать огромное количество методов: непрерывная интеграция, сценарии, виртуальные машины, инструменты для воспроизведения, прокси или перенаправление сетевого трафика.
  • Одно из преимуществ (по крайней мере для меня) инвестирования в тестируемость может быть неочевидным: если тесты утомительны, раздражают или громоздки для написания или запуска, то мне слишком легко просто пропустить их, если на меня оказывают давление или устал. Важно держать ваши тесты ниже порога «Это так просто, что нет оправдания не делать этого».
  • Идеальное программное обеспечение неосуществимо. Как и все остальное, усилия, затраченные на тестирование, являются компромиссом, и иногда они не стоят усилий. Существуют ограничения (такие как отсутствие отдела обеспечения качества). Примите, что ошибки будут, выздоравливайте и учитесь.

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

Джош Келли
источник
7

Я обновил версию библиотеки ... которая ... привела к тому, что метки времени (которые сторонняя библиотека возвращает как long) были изменены с миллисекунд после эпохи на наносекунды после эпохи.

...

Это не ошибка в библиотеке

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

Допустим, вместо этого библиотека возвратила тип MillisecondsSinceEpochпростой оболочки, которая содержит long. Когда они изменили его на NanosecondsSinceEpochзначение, ваш код не смог бы скомпилироваться и, очевидно, указывал бы вам на места, где вам нужно внести изменения. Изменение не может молча испортить вашу программу.

Еще лучше был бы TimeSinceEpochобъект, который мог бы адаптировать свой интерфейс, поскольку была добавлена ​​большая точность, такая как добавление #toLongNanosecondsметода рядом с #toLongMillisecondsметодом, не требуя никаких изменений в вашем коде.

Следующая проблема заключается в том, что у вас нет надежного набора интеграционных тестов для библиотеки. Вы должны написать те. Лучше было бы создать интерфейс вокруг этой библиотеки, чтобы инкапсулировать ее от остальной части вашего приложения. Несколько других ответов касаются этого (и другие продолжают появляться, когда я печатаю). Интеграционные тесты должны выполняться реже, чем ваши юнит-тесты. Вот почему наличие буферного слоя помогает. Разделите ваши интеграционные тесты в отдельную область (или назовите их по-другому), чтобы вы могли запускать их по мере необходимости, но не каждый раз, когда вы запускаете свой модульный тест.

cbojar
источник
2
@ durron597 Я бы все еще утверждал, что это ошибка. Помимо отсутствия документации, зачем вообще менять ожидаемое поведение? Почему не новый метод, который обеспечивает новую точность, и пусть старый метод все еще обеспечивает миллис? И почему бы не предоставить компилятору способ предупредить вас об изменении типа возвращаемого значения? Не требуется много, чтобы сделать это намного понятнее, не только в документации, но и в самом коде.
cbojar
1
@gbjbaanb, «что у них плохая практика выпуска», кажется мне ошибкой
Артуро Торрес Санчес
2
@gbjbaanb Сторонняя библиотека [должна] заключить «договор» со своими пользователями. Нарушение этого контракта - независимо от того, задокументировано оно или нет - может / должно рассматриваться как ошибка. Как уже говорили другие, если вам нужно что-то изменить, добавьте в контракт новую функцию / метод (см. Все ...Ex()методы в Win32API). Если это невозможно, «разорвать» контракт, переименовав функцию (или ее тип возврата), было бы лучше, чем изменить поведение.
TripeHound
1
Это ошибка в библиотеке. Использование наносекунд в течение длительного времени толкает его.
Джошуа
1
@gbjbaanb Вы говорите, что это не ошибка, поскольку это предполагаемое поведение, даже если оно неожиданное. В этом смысле это не ошибка реализации , но ошибка та же. Это можно назвать дефектом дизайна или интерфейсной ошибкой . Недостатки заключаются в том, что он демонстрирует примитивную одержимость длинными, а не явными единицами, его абстракция является утечкой, поскольку он экспортирует детали своей внутренней реализации (что данные хранятся как длинные единицы определенной единицы), и что он нарушает принцип наименьшего удивления с тонкой сменой единиц.
cbojar
5

Вам нужны интеграция и системные тесты.

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

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

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

ptyx
источник
2

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

В противном случае, нет смысла писать модульные тесты с полуопределенными требованиями и слабым пониманием системы.

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

BЈовић
источник
1

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

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

Майк Накис
источник
Это на самом деле не отвечает на вопрос. У меня уже есть слой, отделяющий библиотеку от моей системы, но проблема в том, что мой уровень абстракции может содержать «ошибки», когда библиотека меняет меня без предупреждения.
durron597
1
@ durron597 Тогда, возможно, слой недостаточно изолирует библиотеку от остальной части вашего приложения. Если вы обнаружите, что испытываете трудности с тестированием этого слоя, возможно, вам нужно упростить поведение и более жестко изолировать базовые данные.
cbojar
Что сказал @cbojar. Кроме того, позвольте мне повторить то, что могло остаться незамеченным в приведенном выше тексте: assertключевое слово (или функция, или средство, в зависимости от того, какой язык вы используете,) - ваш друг. Я не говорю об утверждениях в модульных / интеграционных тестах, я говорю, что уровень изоляции должен быть очень тяжелым с утверждениями, утверждая все, что можно утверждать о поведении библиотеки.
Майк Накис
Эти утверждения не обязательно выполняются в производственных процессах, но они выполняются во время тестирования, имея представление белого уровня вашего уровня изоляции и, следовательно, будучи в состоянии убедиться (насколько это возможно), что информация, которую ваш уровень получает из библиотеки это звук.
Майк Накис