Testing Binary File Upload on Jetty

4 декабря 2013

I was developing a small web server using Jetty and Apache Commons FileUpload component when I faced the problem with automated testing of the upload functionality. Jetty has a very nice framework to do tests: ServletTester and HttpTester.

ServletTester tester = new ServletTester();
tester.setContextPath("/");
tester.addServlet(TestingHandler.class, "/");
tester.start();

HttpTester request = new HttpTester();
HttpTester response = new HttpTester();

request.setMethod("GET");
request.setURI("/");

response.parse(tester.getResponses(request.generate()));
assertEquals(200, response.getStatus());

My test had to upload a binary file to check the server-side processing, but I’ve found this is not an easy thing to do. I’ve realized that HttpTester class generate() method produces a String. Okay, let’s use BASE64 to encode the file! But this would not work, because FileUpload supports only «application/octet-stream» content type. Fortunately, FileUpload is an open source product, so I’ve just created a child class of HttpTester with my new generateBytes() method that did the trick. Basically, it is a slightly modified version of HttpTester.generate():

public static class HttpTesterBytes extends HttpTester {
    public HttpTesterBytes() {
        super();
        setHeader("Host", "tester");
    }

    public ByteArrayBuffer generateBytes() throws IOException {
        Buffer bb=new ByteArrayBuffer(32*1024 + (_genContent!=null?_genContent.length:0));
        Buffer sb=new ByteArrayBuffer(4*1024);
        ByteArrayOutputStream2 out = new ByteArrayOutputStream2();
        StreamEndPoint endp = new StreamEndPoint(null, out);

        HttpGenerator generator = new HttpGenerator(new SimpleBuffers(sb, bb),endp);

        if (_method!=null) {
            generator.setRequest(getMethod(),getURI());
            if (_version==null)
                generator.setVersion(HttpVersions.HTTP_1_1_ORDINAL);
            else
                generator.setVersion(HttpVersions.CACHE.getOrdinal(HttpVersions.CACHE.lookup(_version)));
            generator.completeHeader(_fields,false);
            if (_genContent!=null)
                generator.addContent(new View(new ByteArrayBuffer(_genContent)),false);
            else if (_parsedContent!=null)
                generator.addContent(new ByteArrayBuffer(_parsedContent.toByteArray()),false);
        }

        generator.complete();
        generator.flushBuffer();
        return new ByteArrayBuffer(out.toByteArray());
    }
}

To prepare the bytes I’ve made a helper function:

public final static String CONTENT_BOUNDARY = "<--(*)(*)-->"; // Spaceship!

/**
 * Get the binary representation of the file upload
 * @param fieldName     field name to use in the form
 * @param filename      file to load
 * @return              bytes array with file written
 * @throws IOException
 */
public static byte[] getFileRequestContent(String fieldName, String filename, boolean finalize) throws IOException {
    String content = "--" + CONTENT_BOUNDARY + "\r\n" + "Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + filename + "\"\r\n"+
            "Content-Type: application/octet-stream;\r\n\r\n";

    File f = new File(filename);
    byte[] bytes = Files.toByteArray(f);

    ByteArrayOutputStream stream = new ByteArrayOutputStream2();
    stream.write(content.getBytes());

    stream.write(bytes);

    String finish = "\r\n";
    if (finalize)
        finish += "--" + CONTENT_BOUNDARY + "--\r\n\r\n";

    stream.write(finish.getBytes());
    stream.flush();

    return stream.toByteArray();
}

And now how it looks in the test:

HttpTesterBytes request = new HttpTesterBytes();
HttpTester response = new HttpTester();

request.setMethod("POST");
request.setHeader("Content-Type", "multipart/form-data; boundary=" + CONTENT_BOUNDARY);

request.setContentBytes(getFileRequestContent("data_file, "test/Nocturne.jpg"));

response.parse(tester.getResponses(request.generateBytes()).array());

assertTrue(response.getMethod() == null);
assertEquals(200, response.getStatus());
assertTrue(response.getContent().contains("cute!"));

Абсолютное добро: автоматические тесты

31 марта 2013

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

Для себя я выделил 4 жирных плюса автоматических тестов:

  1. Функциональность, покрытая тестами, гарантированно работает,
  2. и это выясняется очень быстро, а попутно можно делать замеры производительности;
  3. Тестовые инженеры избавлены от рутинной работы и могут ловить действительно сложные ошибки;
  4. Достаточно просто смоделировать стресс-тестирование — просто увеличить количество/интенсивность тестов.

Звучит вроде хорошо, но как это все сделать на практике? В «Концепторе» мы сначала ни о чем таком не думали — писали код с максимально возможной скоростью, чтобы как можно скорее получить хоть что-то рабочее. Но в определенный момент стало понятно, что кода уже слишком много, чтобы держать его в голове целиком, а проблемы могут проявиться не сразу. Тогда мы взяли в руки джанговские тесты и принялись доводить покрытие до разумного уровня. Получилось около 98% на важных файлах – возможно, перегиб, но после этого мы могли совершенно не волноваться о том, что какая-то смелая правка сломает, например, систему авторизации и регистрации. Для анализа покрытия мы пользовались Django coverage. Также рекомендую хорошую статью про Selenium WebDriver, которой пользуется Yandex для тестирования веб-интерфейса своей почты. В Java, на которой я программирую сейчас, мы используем JUnit. Кстати, PyCharm и IntelliJ Idea имеют встроенную поддержку просмотра покрытия кода тестами.

Но самое трудное — это, конечно, перейти из состояния «нифига нет, а нужно немеряно работы» в состояние «о, как у нас все круто» — обычно это происходит через состояние «ну, что-то уже есть». Легче, конечно, вздохнуть и отложить все в долгий ящик (испытанный способ похоронить хорошую идею). Потому что начальство кричит и все нужно вчера, как обычно. Сразу писать сотни тестов очень тяжело, хотя мы пошли именно по такому пути, и это заняло около двух с половиной недель работы на полной занятости (все-таки тесты пишутся довольно быстро). Можно начать постепенно, а самое лучшее — посадить за написание/обновление тестов нового участника команды. Два в одном — и в коде можно разобраться, и пользу принести. Можно начать потихоньку писать самому, в режиме «лучший отдых — смена деятельности». А руководству объяснить, что проще и дешевле устранять ошибки на самых ранних стадиях, чем выслушивать крики и угрозы пользователей.

Ну и, конечно, тесты нужно поддерживать в актуальном состоянии, но немного дисциплины очень полезно для программерского здоровья.

Кое-что о тестировании

23 ноября 2010

За время своей работы в IT я сталкивался с очень разным отношением к тестированию ПО. Хочу поделиться своими наблюдениями по этой теме.

Цена ошибки

Я встречал два подхода к тестированию: must have и «разработчики все сделают как надо и сами проверят». Почему-то среднего не наблюдалось.
Первый подход я наблюдал в двух компаниях, работающих в сфере телекоммуникаций, только одна разрабатывала собственно пользовательские мобильные устройства, а другая – сети для этих устройств. Тестовый процесс в них так же серьезен, как и разработческий: контроль того, что все требования покрыты тестами, несколько фаз тестирования – проверка работоспособности старых функций после интеграции нового кода (LegacyRegression/Sanity/Integration Test), проверка новой функциональности (System/Function Test), стресс-тестирование и так далее. Такой подход обусловлен следующими причинами:

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

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

Читать дальше…