2009-06-14 2 views
2

Ниже приведенный ниже тестовый класс проверяет, что простой HttpService получает контент из заданного URL-адреса. Обе приведенные выше реализации демонстрируют прохождение теста, хотя одно явно неверно, потому что он создает URL-адрес с неправильным аргументом.Groovy: проверьте построение зашитого URL-адреса

Чтобы избежать этого и правильно указать поведение, которое я хочу, я хотел бы проверить, что в блоке использования тестового примера я построю один (и только один) экземпляр класса URL и что аргумент url для конструктора правильно. A Groovy enhancement кажется, что это позволит мне добавить заявление

mockURLContext.demand.URL { assertEquals "http://www.foo.com", url } 

, но что я могу обойтись без этого Groovy повышения?

Обновление: Заменено «mock» с «заглушкой» в названии, так как меня интересует только проверка состояния, а не детализации взаимодействий. У Groovy есть механизм StubFor, который я не использовал, поэтому я оставлю свой код так, как есть, но я думаю, вы могли бы просто заменить MockFor на StubFor повсюду.

import grails.test.* 
import groovy.mock.interceptor.MockFor 

class HttpServiceTests extends GrailsUnitTestCase { 
    void testGetsContentForURL() { 
     def content = [text : "<html><body>Hello, world</body></html>"] 

     def mockURLContext = new MockFor(URL.class) 
     mockURLContext.demand.getContent { content } 
     mockURLContext.use { 
      def httpService = new HttpService() 
      assertEquals content.text, httpService.getContentFor("http://www.foo.com") 
     } 
    } 
}  

// This is the intended implementation. 
class HttpService { 
    def getContentFor(url) { 
     new URL(url).content.text 
    } 
} 

// This intentionally wrong implementation also passes the test! 
class HttpService { 
    def getContentFor(url) { 
     new URL("http://www.wrongurl.com").content.text 
    } 
} 

ответ

3

Что насмехается над URL-адресом? Это затрудняет запись теста. Вы не сможете отреагировать на обратную связь, которые макетные объекты дают вам о дизайне API класса URL, потому что он не под вашим контролем. И если вы точно не подделываете поведение URL-адреса и то, что он предоставляет по протоколу HTTP, тест не будет надежным.

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

Для HTTP Я пишу тест, который создает HTTP-сервер, подключает сервлет к серверу, который возвращает некоторые консервированные данные, передает URL-адрес сервлета объекту, чтобы он загружал данные, проверял, что загруженный результат это то же самое, что и законсервированные данные, используемые для инициализации сервлета, и остановка сервера в срывке прибора. Я использую Jetty или простой HTTP-сервер, который поставляется вместе с JDK 6.

Я использовал только макеты для проверки поведения объектов, которые общаются с интерфейсом (-ами) этого объекта.

+0

Вы описываете интеграционные тесты, которые проверяют правильное использование HTTP, что, конечно, очень полезно. Но они будут работать медленно, благодаря этой настройке и разрыву. Я хочу запустить тысячи модульных тестов за считанные секунды и только затем выполнить более медленные интеграционные тесты, чтобы получить дополнительную обратную связь. Итак, как я могу проверить, что я написал HttpService в основном правильным образом, прежде чем переходить к более подробным (и важным) тестам интеграции, которые вы описываете? –

+0

Я понял, благодаря этому ответу, что я должен был ссылаться на «заглушки» в вопросе, а не «издеваться», поскольку простая запись вызовов и проверка состояния были бы совершенно прекрасными (см. Http: // martinfowler .com/статьи/mocksArentStubs.html). Я уточню вопрос. –

+1

Единственный способ узнать, правильно ли работает HttpService, - это дублировать поведение класса URL и всей скрытой инфраструктуры, которая находится за ней _absolutely точно. Вы действительно думаете, что можете это сделать? Вы точно знаете, как это реализовано? Более того, вы действительно думаете, что это стоит того? И что, если он изменится в будущей версии? – Nat

0

Что именно вы ожидаете, чтобы потерпеть неудачу? Не легко понять, что вы пытаетесь проверить с помощью этого кода. По Mocking URL.getContent вы сообщаете Groovy, чтобы всегда возвращать переменную content, когда URL.getContent() is invoked. Желаете ли вы вернуть значение URL.getContent() на основе строки URL? Если это так, то следующее Достигается это:

import grails.test.* 
import groovy.mock.interceptor.MockFor 

class HttpServiceTests extends GrailsUnitTestCase { 
    def connectionUrl 

    void testGetsContentForURL() { 
     // put the desired "correct" URL as the key in the next line 
     def content = ["http://www.foo.com" : "<html><body>Hello, world</body></html>"] 

     def mockURLContext = new MockFor(URL.class) 
     mockURLContext.demand.getContent { [text : content[this.connectionUrl]] } 
     mockURLContext.use { 
      def httpService = new HttpService() 
      this.connectionUrl = "http://www.wrongurl.com" 
      assertEquals content.text, httpService.getContentFor(this.connectionUrl) 
     } 
    } 
} 
+0

Хммм. Я следую его вопросу, но не ваш ответ. Он пытается высмеять класс URL, а не его HTTPService. HTTPService - это тестируемый код. – cwash

+0

Я удалил эту часть комментария. Причина, по которой я спрашивал об этом, заключается в том, что я не могу сказать, что пытается сделать оригинальный плакат с приведенным здесь кодом и описанием. Код, который я предоставил, передаст случай, который, как ожидается, пройдет и завершится неудачно. Что это действительно доказывает? – laz

+0

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

0

Это трудно дразнить из JDK классов, которые заявлены окончательная ... Ваша проблема, как вы ссылки через аксессуар, является то, что нет никакого способа, чтобы создать URL, отличный от вызова конструктора.

Я пытаюсь отделить создание этих объектов от остальной части моего кода; Я бы создал фабрику, чтобы отделить создание URL-адресов. Это должно быть достаточно простым, чтобы не требовать проверки. Другие используют типичный подход обертки/декоратора. Или вы можете применить adapter pattern для перевода на объекты домена, которые вы пишете.

Вот подобный ответ на удивительно подобную проблему: Mocking a URL in Java

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

Надеюсь, это поможет.

+0

Вы правильно описываете стандартные обходные пути для неспособности Java издеваться над новыми вызовами(), но они, как правило, обманывают относительно простые случаи, подобные этому. Я никогда не собираюсь использовать альтернативный способ получения контента URL, поэтому URLFactory не добавляет ничего к моему дизайну и на самом деле делает код более сложным для выполнения (вам нужно искать и читать URLFactory.groovy, чтобы быть уверенным он не делает ничего странного). MockFor избегает этого во многих случаях, например, в примере теста, где я проверяю вызов content() напрямую, без ввода каких-либо фабрик. Мне просто нужна такая же польза для конструкторов. –

+0

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

+0

Согласовано. Я просто пытался ответить на ваш вопрос, как бы вы это делали * без * улучшения в Groovy. Я не сказал, что все будет хорошо. :) В чистом Java-коде вы можете использовать JMockit для выполнения того же самого. Это может быть немного сложно правильно издеваться над классами JDK с этим, но выполнимо почти так же. – cwash

2

Надеюсь, что мой вариант программирования «Программирование в малом» и «Unit test 100%» вам может считать это единственным методом, который делает слишком много вещей. Вы можете реорганизовать HTTPService к:

class HttpService { 
    def newURLFrom(urlString) { 
    new URL(urlString) 
    } 
    def getContentText(url) { 
    url.content.text 
    } 
    def getContentFor(urlString) { 
    getContentText(newURLFrom(urlString)) 
    } 
} 

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

class HttpServiceTests extends GroovyTestCase { 

    def urlString = "http://stackoverflow.com" 
    def fauxHtml = "<html><body>Hello, world</body></html>"; 
    def fauxURL = [content : [text : fauxHtml]] 

    void testMakesURLs() { 
    assertEquals(urlString, 
       new HTTPService().newURLFrom(urlString).toExternalForm()) 
    } 
    void testCanDeriveContentText() { 
    assertEquals(fauxHtml, new HTTPService().getContentText(fauxURL)); 
    } 
    // Going a bit overboard to test the line combining the two methods 
    void testGetsContentForURL() { 
    def service = new HTTPService() 
    def emc = new ExpandoMetaClass(service.class, false) 
    emc.newURLFrom = { input -> assertEquals(urlString, input); return fauxURL } 
    emc.initialize() 
    service.metaClass = emc 
    assertEquals(fauxHtml, service.getContentFor(urlString)) 
    } 
} 

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

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

+0

+1 - Это, по-видимому, является альтернативой оригинальному предлагаемому методу, использующему улучшенное улучшение. Я думаю, что имеет смысл сказать, что часто ваш подход к тому, чтобы сделать вещи более проверкими, будет иметь набор компромиссов, которые необходимо учитывать. Тесты должны быть тщательными, но экономичными. – cwash

Смежные вопросы