Преамбула.
Как-то летом один персонаж сказал, что он собирается встроить тесты внутрь самого приложения. Тогда я молча поржал про себя.
Спустя 3 месяца на Agile Testing Days я побывал на докладе, где рассказывалось о том что Runtastic делал так для того чтобы выявить багу на устройствах пользователей.
The developers decided to build a unit test into the app where they could check the result of the device’s calculation for a well-known distance between two coordinates.(Чуть более подробно можно прочесть тут, но большая часть отводится рекламе)
Собственно с того самого доклада мысль зудела в голове и вылилась в пару часов пляски с кодом.
Амбула.
Начнем мы конечно же с первого по важности для аудитории этого уютненького бложека вопроса -Если вам не достаточно кейса Runtastic приведенного выше, то отвечу.
Если у вас все хорошо, production вам подконтролен, конфигурация зафиксирована и проблем нет - оно вам не нужно.
Но не у всех и не всегда оно так.
Примеры когда оно не так:
- Вы разрабатываете что-то что будет ставится в непонятное окружение или контейнер и вам нужно понимать что окружение соответствует.
- Окружение в которое вы ставите свое поделие может динамически меняться и об этом тоже было бы неплохо знать.
В общем и целом оба вопроса выше сводятся к вопросу о доверии к reference implementation чего-то что вам дают снаружи ( в случае с Runtastic таким reference implementation-ом было API по работе с GPS видимо).
В зависимости от того насколько большому куску мы не доверяем может захотеться написать как Unit-тест, так и интеграционный.
Рассмотрим оба случая :).
Естественно я буду рассматривать это все на Java потому что
Покажи мне свой код.
Да детка, обнажи свой свежий, горячий код и я сделаю тебе ревью!
Давай делать это бесстыдно! Давай делать это на гитхабе!
Давай делать это бесстыдно! Давай делать это на гитхабе!
Код собственно на гитхабе.
Клонируем, в консоли запускаем mvn exec:java, видим такое.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[INFO] ------------------------------------------------------------------------ | |
[INFO] Building embedded-unittests 1.0-SNAPSHOT | |
[INFO] ------------------------------------------------------------------------ | |
[INFO] | |
[INFO] --- exec-maven-plugin:1.3.2:java (default-cli) @ embedded-unittests --- | |
[WARNING] Warning: killAfter is now deprecated. Do you need it ? Please comment on MEXEC-6. | |
2014-12-09 16:11:19.748:INFO:oejs.Server:jetty-8.1.9.v20130131 | |
2014-12-09 16:11:19.784:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080 |
http://127.0.0.1:8080/runUnitTests - для запуска Unit тестов
Видим такое
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Total 2 Failures 1 | |
testB(test.TestB): java.lang.AssertionError | |
at org.junit.Assert.fail(Assert.java:86) | |
at org.junit.Assert.assertTrue(Assert.java:41) | |
at org.junit.Assert.assertTrue(Assert.java:52) | |
at test.TestB.testB(TestB.java:12) | |
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) | |
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) | |
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) | |
at java.lang.reflect.Method.invoke(Method.java:606) | |
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47) | |
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) | |
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44) | |
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) | |
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271) | |
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70) | |
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50) | |
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238) | |
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63) | |
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) | |
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53) | |
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229) | |
at org.junit.runners.ParentRunner.run(ParentRunner.java:309) | |
at org.junit.runners.Suite.runChild(Suite.java:127) | |
at org.junit.runners.Suite.runChild(Suite.java:26) | |
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238) | |
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63) | |
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) | |
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53) | |
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229) | |
at org.junit.runners.ParentRunner.run(ParentRunner.java:309) | |
at org.junit.runner.JUnitCore.run(JUnitCore.java:160) | |
at org.junit.runner.JUnitCore.run(JUnitCore.java:138) | |
at UnitTestRunner.doGet(UnitTestRunner.java:28) | |
at javax.servlet.http.HttpServlet.service(HttpServlet.java:735) | |
at javax.servlet.http.HttpServlet.service(HttpServlet.java:848) | |
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:669) | |
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:457) | |
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1075) | |
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:384) | |
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1009) | |
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135) | |
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116) | |
at org.eclipse.jetty.server.Server.handle(Server.java:368) | |
at org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:488) | |
at org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:932) | |
at org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:994) | |
at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:640) | |
at org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235) | |
at org.eclipse.jetty.server.AsyncHttpConnection.handle(AsyncHttpConnection.java:82) | |
at org.eclipse.jetty.io.nio.SelectChannelEndPoint.handle(SelectChannelEndPoint.java:628) | |
at org.eclipse.jetty.io.nio.SelectChannelEndPoint$1.run(SelectChannelEndPoint.java:52) | |
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608) | |
at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543) | |
at java.lang.Thread.run(Thread.java:744) |
http://127.0.0.1:8080/runIntegrationTests - для запуска интеграционных тестов.
Видим такое
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Total 2 Failures 1 | |
testA(itest.ITestA): expected:<[a]> but was:<[b]>org.junit.ComparisonFailure: expected:<[a]> but was:<[b]> | |
at org.junit.Assert.assertEquals(Assert.java:115) | |
at org.junit.Assert.assertEquals(Assert.java:144) | |
at itest.ITestA.testA(ITestA.java:19) | |
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) | |
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) | |
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) | |
at java.lang.reflect.Method.invoke(Method.java:606) | |
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47) | |
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) | |
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44) | |
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) | |
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271) | |
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70) | |
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50) | |
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238) | |
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63) | |
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) | |
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53) | |
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229) | |
at org.junit.runners.ParentRunner.run(ParentRunner.java:309) | |
at org.junit.runners.Suite.runChild(Suite.java:127) | |
at org.junit.runners.Suite.runChild(Suite.java:26) | |
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238) | |
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63) | |
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) | |
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53) | |
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229) | |
at org.junit.runners.ParentRunner.run(ParentRunner.java:309) | |
at org.junit.runner.JUnitCore.run(JUnitCore.java:160) | |
at org.junit.runner.JUnitCore.run(JUnitCore.java:138) | |
at IntegrationTestRunner.doGet(IntegrationTestRunner.java:36) | |
at javax.servlet.http.HttpServlet.service(HttpServlet.java:735) | |
at javax.servlet.http.HttpServlet.service(HttpServlet.java:848) | |
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:669) | |
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:457) | |
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1075) | |
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:384) | |
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1009) | |
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135) | |
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116) | |
at org.eclipse.jetty.server.Server.handle(Server.java:368) | |
at org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:488) | |
at org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:932) | |
at org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:994) | |
at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:640) | |
at org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235) | |
at org.eclipse.jetty.server.AsyncHttpConnection.handle(AsyncHttpConnection.java:82) | |
at org.eclipse.jetty.io.nio.SelectChannelEndPoint.handle(SelectChannelEndPoint.java:628) | |
at org.eclipse.jetty.io.nio.SelectChannelEndPoint$1.run(SelectChannelEndPoint.java:52) | |
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608) | |
at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543) | |
at java.lang.Thread.run(Thread.java:744) |
Мысли вслух.
Unit-тесты встроенные в приложение позволят вам отловить исключительно мелкие вещи. Вреда от того что они встроены в само приложение не должно быть никакого, разве что только если у вас их там миллион и вы все их будете гонять на production-среде.В случае с интеграционными тестами все несколько сложнее. Во-первых для того чтобы они были интеграционными вам их надо интегрировать, как бы ни глупо это звучало.
В приведенном примере кода тесты получают тот же самый инстанс Injector-а Guice, что и основное приложение.
Можно конечно сделать отдельный или шарить между инжекторами/контекстами только нужные вещи - но это все детали конкретной реализации.
Второй интересный момент который я нашел именно в такой реализации заключается в том, что подключение реальных кусков бизнес-логики к интеграционным тестам может помочь ловить баги, которые воспроизводятся только в определенных условиях.
Минусы тоже имеются - если в ваши слои бизнес-логики можно вносить изменения в runtime, то запуск таких интеграционных тестов может положить вам ваш production.
Еще более просто и конкретно - такая штука может положить нафиг весь ваш prod и данные.
И да - подобного рода трюки не предусматривают никакой защиты от этого кроме code review.
Третье - такой подход тянет за собой все зависимости необходимые для тестов в основной продукт - то есть всякие junit, mockito, hamcrest. Ну и код тестов конечно.
Избежать этого можно - в своем примере я развел это через профили сборки maven.
Единственная корявость которая есть - приходится указывать значение свойства по умолчанию для исключаемых при компиляции пакетов.
При обычной сборке (mvn clean package) с такой настройкой содержимое архива будет выглядеть так.
А при релизной (mvn clean package -P release) вот так
По аналогии можно сделать и с зависимостями - их скоуп тоже можно разрулить через профили сборки.
P.S. Как и обещал.
Хохлома.
Гимназистки.
Комментариев нет:
Отправить комментарий