Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 
Показаны сообщения с ярлыком junit. Показать все сообщения
Показаны сообщения с ярлыком junit. Показать все сообщения

суббота, 8 апреля 2023 г.

Новый вид unit тестирования - Directive Approvals Testing

Люблю свой Сodenjoy проект потому, что могу в нем пробовать новые подходы в разработке. Сегодня хочу продемонстрировать один из них. Ведь именно так и рождаются инженерные практики: ты экпериментируешь на своей кухне => что-то показывает хороший результат => ты это осознаешь => публикуешь открытие => его уносят в массы.

Вот пример использования одного кастомного малого, которого я научил бегать по разным jar расположенным как в класспасе, так и за пределами проекта (рядом с war например в папке с плагинами).

@Configuration
public class MVCConf implements WebMvcConfigurer {

    @Bean
    @SneakyThrows
    public ResourceHttpRequestHandler resourceHttpRequestHandler(ServletContext servletContext) {
        return new ResourceHttpRequestHandler() {{
            setCacheControl(getCache());

            setLocations(Arrays.asList(
                    // only for testing so that you can get resources from src/target folder
                    new UrlResource("file:../games/*/src/main/**"),
                    new UrlResource("file:src/main/**"),
                    new UrlResource("file:target/classes/**"),
                    // production code
                    new ServletContextResource(servletContext, "/resources/"),
                    new ClassPathResource("classpath:/resources/"),
                    new ClassPathResource("classpath*:**/resources/"),
                    new UrlResource("file:" + pluginsStatic),
                    new UrlResource("file:" + pluginsResources)));

            setResourceResolvers(Arrays.asList(new JarPathResourceResolver()));
        }};
    }

Тут можно заметить один интересный сайд эффект. Если во время разработки натравить его на папку src/target (чтобы он искал сперва там), то можно будет править скрипты и без пересборки приложения видеть изменения в браузере. Это секономит тебе дни времени за недели разработки. Достаточно будет отключить кеши в браузере или нажать Ctrl-F5.

Но вернемся к тестам для этого малого. Ничего примечательного. Все как обычно. Достаточно хорошо читаемо. Только меня смущает тот самый CopyPast который я всегда делал, чтобы создать базу для нового теста. А раз есть CopyPast, значит есть пространство для Рефакторинга.

public class JarPathResourceResolverTest {

    private JarPathResourceResolver resolver = new JarPathResourceResolver();

    @SneakyThrows
    public String load(Resource resource)  {
        return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
    }

    @Test
    public void shouldLoadFromServletContext() {
        // when
        Resource resource = resolver.getResource("file1.txt",
                new ServletContextResource(new MockServletContext(), "/resolver/"));

        // then
        assertEquals("ServletContext resource [/resolver/file1.txt]", resource.toString());
        assertEquals("one", load(resource));
    }

    @Test
    public void shouldLoadFromClasspath() {
        // when
        Resource resource = resolver.getResource("file2.txt",
                new ClassPathResource("classpath:/resolver/"));

        // then
        assertEquals("class path resource [resolver/file2.txt]", resource.toString());
        assertEquals("two", load(resource));
    }

    @Test
    public void shouldLoadFromClasspathIncludingJars() {
        // when
        Resource resource = resolver.getResource("NOTICE",
                new ClassPathResource("classpath*:META-INF/"));

        // then
        assertMatch("URL [jar:file:*.jar!/META-INF/NOTICE]", resource.toString());
        assertEquals(true, resource.exists());
    }

    @Test
    public void shouldLoadFromFileSystem() throws Exception {
        // when
        Resource resource = resolver.getResource("file3.txt",
                new UrlResource("file:src/test/resources/resolver/"));

        // then
        assertEquals("URL [file:src/test/resources/resolver/file3.txt]", resource.toString());
        assertEquals("three", load(resource));
    }

    @Test
    public void shouldLoadFromJarsInFileSystem_case1() throws Exception {
        // when
        Resource resource = resolver.getResource("file4.txt",
                new UrlResource("file:src/test/resources/resolver/*.jar!/resources/**/"));

        // then
        assertMatch("URL [jar:file:*/server/src/test/resources/resolver/jar4.jar!/resources/subfolder/file4.txt]", resource.toString());
        assertEquals("four", load(resource));
    }

    @Test
    public void shouldLoadFromJarsInFileSystem_case2() throws Exception {
        // when
        Resource resource = resolver.getResource("file5.txt",
                new UrlResource("file:src/test/resources/resolver/*.jar!/resources/**/"));

        // then
        assertMatch("URL [jar:file:*/server/src/test/resources/resolver/jar5.jar!/resources/file5.txt]", resource.toString());
        assertEquals("five", load(resource));
    }
}

Не секрет, что я люблю использовать в своем проекте approvals подход. Единожды подглянутый на какой-то конференции много лет назад в одноименной библиотеке я сразу осознал, что мы с ним на долго. Больше не будет никаких assertEquals(42, godObject.getMainQuestion()); и последующих медитирований над одиноко слетевшим jUnit ассертом без понимания "а что там дальше было-то". Нет, я просто возьму и допишу объекту toString() метод (если не могу на проде - сделаю toString(godObject) в процедурном стиле прям в тестовом классе) и далее буду:
assertEquals("В книге Дугласа Адамса «Путеводитель для путешествующих автостопом по галактике»\n"
             "ответ на «Главный вопрос жизни, вселенной и вообще» должен был решить все проблемы Вселенной.\n"
             "Этого ответа с нетерпением ждали все разумные расы.\n" + 
             "Он был получен в результате семи с половиной миллионов лет непрерывных \n" + 
             "вычислений на специально созданном компьютере — Думателе.\n" + 
             "По утверждению компьютера, ответ был несколько раз проверен на правильность, \n" + 
             "но он может всех огорчить. Оказалось, что ответ на вопрос — «42».", godObject.toString());

Теперь, если у меня слетит тест, я буду видеть diff всего стейта объекта, а не малоинформативное "Expected: 42 But was: 43". Я всегда раньше думал глядя на такие ассерты как-то так:

На основе этого approvals подхода сделано много кастомных решений в проекте. Для Smoke тестов например я использую даже стенографию каких-то важных мне аспектов системы в плоский файл. 

С последующим сравниванием двух файлов (expected / actual) с помощью встроенной в IDE diff тулы. Конечно этот файл никогда не правится вручную - я лишь смотрю на изменения которые он мне подсветил и либо approve (отсюда название подхода approvals) их либо лезу в код править что-то, что я не учел. 

Так, пока diff не будет таким, который я ожидаю. Лишь тогда я смогу старый expected файл заменить новым actual, сгенерированным во время последнего запуска. Его я и закоммичу как новый expected слепок. 

Но вернемся к инсайту сегодняшнего дня. Базируясь на этом подходе я пошел дальше и захотел переписать юнит тест в какой-то такой вид. 

Все это может чуть-чуть напугать. Ведь во-первых формат непонятный. Во вторых посмотрите на эти строчки - кто будет потом суппортить такой длинный ассерт? Но спешу успокоить - я никогда не буду править этот текст ручками - буду копировать результат из diff тулзы в случае исправления и вставлять его между двух кавычек "". В этом суть approvals подхода. Я смотрю diff - я вижу, что отклик теста на мои исправления в системе адекватен, и я применяю actual как новый expected. 

А формат прост: причина=>следствие=>следствие.

[SERVLET|CLASSPATH|URL] location file-to-find=>resource.toString()|NULL=>file-content|NULL|EXISTS

Каждая такая строчка одновременно и инструкция как запускать тестируемый класс (SERVLET - используй ServletContextResource, CLASSPATH - ClassPathResource, URL - UrlResource), и директивы как проверять результат (NULL - ожидается что ресурс не найден, EXISTS - не грузим файл полностью, а только проверяем его существование). 

А если в блоке resource.toString() встречается "*" - то использовать не assertEquals, а assertMatch, который проверит соответсвует ли строчка "qwertyu" заявленному паттерну "qw*yu", что тоже удобно - потому как resource.toString() выдает часто полный путь включая c:\\java\\... что сделает тесты чувствительными не только к системе но и к местоположению проекта. 

Короче сделал свой DSL и запаковал его прямиком в expected текст. Магия подхода в том, что выполнение этих команд сгенерирует точно такой же по формату actual блок команд, только с подставленными runtime результатами. А IDE останется только показать diff этих двух версий. И напомню, если мне понравится actual, я скопипащу его в expected теста. Если нет - я отправлюсь в код программы и буду править. 

Например я тут я сразу вижу, что ServletContextResource работает, а вот с ClassPathResource и UrlResource беда - они все не хотят искать файлы.

А вот тут я вижу, что как-то криво контактенируется искомый файл и контекст в котором ищем и там появляется лишний слеш, а потому некоторые кейзы поиска не отрабатывают

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

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

Опять же, в чистом approvals подходе я никогда не пишу этот весь контент сам. Я запускаю пустой ассерт.
    @Test
    public void shouldAddTrainingSlash() {
        assertAll("");
    }

Далее копирую Actual из diff тулы IDE и вставляю его в пустые кавычки, но только если вижу, что там все ок. Если не ок - одно из двух: либо тестовый фреймворк с ошибкой, либо я нашел ошибку в продакшен коде. 

Но в новом подходе надо указать какие-то директивы к запуску. Это обязывает использовать свой паттерн. Благо если уже есть какие-то наработки то Ctrl-D и скопировать строчку и поправить не сложно. А когда есть какой-то сет - дальше пользуемся CopePast Actual => Expected.

Кстати вот код, который генерирует это все безобразие. 

    private void assertAll(String data) {
        assertEquals(data,
                Arrays.stream(data.split("\n"))
                        .peek(line -> log.info(String.format("Processing: '%s'\n", line)))
                        .map(line -> line.split("=>"))
                        .map(array -> array[0] + "=>" + call(array[0], array[1], array[2]))
                        .collect(joining("\n")) + "\n");
    }

    private String call(String request, String expectedResource, String expectedContent) {
        String[] parts = request.split(" ");
        String type = parts[0];
        String location = parts[1];
        String file = parts[2];

        // when
        JarPathResourceResolver resolver = new JarPathResourceResolver();
        Resource resource = resolver.getResource(file, withResource(type, location));

        // then
        return String.format("%s=>%s",
                getResource(expectedResource, resource),
                getContent(expectedContent, resource));
    }

    @SneakyThrows
    private Resource withResource(String type, String location) {
        switch (type) {
            case "SERVLET":
                return new ServletContextResource(new MockServletContext(), location);
            case "CLASSPATH":
                return new ClassPathResource(location);
            case "URL":
                return new UrlResource(location);
            default:
                throw new IllegalArgumentException("Unknown type: " + type);
        }
    }

    private String getResource(String expectedResource, Resource resource) {
        if (expectedResource.contains("*") && isMatch(expectedResource, toString(resource))) {
            return expectedResource;
        }
        return toString(resource);
    }

    private String toString(Resource resource) {
        if (resource == null) {
            return "NULL";
        }
        return resource.toString().replaceAll("\\\\", "/");
    }

    private String getContent(String content, Resource resource) {
        if (resource == null) {
            return "NULL";
        }
        String exists = resource.exists() ? "EXISTS" : "NOT EXISTS";
        return content.equals("EXISTS") ? exists : load(resource);
    }

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

Ах да! Надо назвать как-то этот подход. Имя твое - Directive Approvals Testing.

Все это было написано среди ночи под приятные слуху колебания воздуха в Patrik Pietschmann. Отдельно хочу отметить эту композицию другого Автора. Такое количества нот я не видел даже у Рахманинова, хотя тут больше гаммы, а у Раманинова тотальный рендом )

Кстстати небольшой бонус для тех, кто дочитал до этого места. Пример assertMatch как расширение assertEquals с проверками типа qwertyu == qw*yu. Очень полезно, если надо из expected блока скрыть часть текста, который либо очень недетерминированный либо выдает какие-то особенности dev-окружения закреплять которые в тесте конечно же не стоит.

    public static boolean isMatch(String expectedPattern, String actual) {
String[] patterns = expectedPattern.split("\\*", -1);

String first = patterns[0];
if (!first.isEmpty()
&& !actual.startsWith(first))
{
return false;
}

String last = patterns[patterns.length - 1];
if (patterns.length > 1
&& !last.isEmpty()
&& !actual.endsWith(last))
{
return false;
}

int pos = 0;
for (String pattern : patterns) {
int index = actual.indexOf(pattern, pos);
if (index < pos) {
return false;
}
pos = index + pattern.length();
}

return true;
} public static void assertMatch(String pattern, String actual) { if (!isMatch(pattern, actual)) { assertEquals(pattern, actual); } }
И тесты для него
    @Test
public void testIsMatch_case1() {
assertEquals(true, isMatch("qwe-asd-*", "qwe-asd-zxc"));
assertEquals(false, isMatch("qwe-Asd-*", "qwe-asd-zxc"));
assertEquals(true, isMatch("*-asd-zxc", "qwe-asd-zxc"));
assertEquals(false, isMatch("*-aSd-zxc", "qwe-asd-zxc"));
assertEquals(true, isMatch("*-asd-*", "qwe-asd-zxc"));
assertEquals(false, isMatch("*-asd-*A", "qwe-asd-zxc"));
assertEquals(true, isMatch("*-*-*", "qwe-asd-zxc"));
assertEquals(false, isMatch("A*-*-*", "qwe-asd-zxc"));
assertEquals(true, isMatch("*", "qwe-asd-zxc"));
assertEquals(false, isMatch("*A", "qwe-asd-zxc"));
assertEquals(true, isMatch("qwe-*-zxc", "qwe-asd-zxc"));
assertEquals(false, isMatch("qwe-A*-zxc", "qwe-asd-zxc"));
assertEquals(true, isMatch("q*-*-zx*-wer", "qwe-asd-zxc-wer"));
assertEquals(false, isMatch("q*-*-Zx*-wer", "qwe-asd-zxc-wer"));

assertEquals(true, isMatch("a*c", "abbbc"));
assertEquals(false, isMatch("a*b", "abbbc"));
assertEquals(true, isMatch("*a*", "banana"));
assertEquals(true, isMatch("*a*", "ananas"));
assertEquals(true, isMatch("**a*", "ananas"));
assertEquals(true, isMatch("*a**", "ananas"));
assertEquals(true, isMatch("**a**", "ananas"));
assertEquals(true, isMatch("*a*b", "ab"));
assertEquals(true, isMatch("*a*b*", "acbabcab"));
assertEquals(true, isMatch("a*", "a"));
assertEquals(true, isMatch("a", "a"));
assertEquals(true, isMatch("*a", "ba"));
assertEquals(true, isMatch("*", ""));
assertEquals(true, isMatch("a*b", "acbabcab"));
assertEquals(true, isMatch("a**b", "acbabcab"));
assertEquals(true, isMatch("a***b", "acbabcab"));
assertEquals(false, isMatch("b*", "ab"));
assertEquals(false, isMatch("b**", "ab"));
assertEquals(false, isMatch("*a", "b"));
assertEquals(false, isMatch("**a", "b"));
assertEquals(false, isMatch("*a*b", "abbc"));
assertEquals(true, isMatch("*a*b*", "acbabc"));
assertEquals(true, isMatch("*a*c", "abbbc"));
assertEquals(true, isMatch("*a*c*", "acbabc"));
}
Приятного аппетита! 
 
Ах да, этот код/и тесты был сгенерирован вредным ChatGPT, который все никак не хотел слушаться. Ему помог GithubCopilot в двух корнер кейзах. Будь внимателен. LLM моделям снится как они видели как кто-то кодировал когда-то. Именно этот "сон" ты видишь во время общения с ними. В их "коде" и любых утверждениях сосредоточено много ошибок, хотя сказано это с умным видом лица. Я бы сказал 60-80% информации верны - что уже очень-очень неплохо. На остальные % пиши тесты. Много тестов не бывает. О моем опыте промптинга я раскажу в другом посте. 

Ах да, и делай Self Code Review перед каждым своим коммитом. Так, на всякий.

понедельник, 16 марта 2020 г.

SmartAssert или как проверять статус всех assertEquals в одном тесте

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

Код лежит тут. Лицензия GNU GPL v3. На здоровье.

Если тебе лень писать @Test public void shouldBlahBlahBlah_whenBlahBlahBlah () {} для каждого отдельного ассерта, то скорее всего у тебя несколько ассертов в одном тесте. Так не рекомендуют делать, но это решение встречается достаточно часто. Почему? Просто потому что блок // given в этом конкретном тесте с 3-5-7ю ассертами может быть большим. А копипастить его в каждый отдельный тест чтобы уютно разместить там 1 assertEquals еще большее зло.

Да, конечно можно выделить // given часть теста в отдельный метод и повторно использовать. Только фигня получится. Вот был лаконичный тест: в // given подготовились, в // when вызвали одну строчку тестируемого метода, и в 3-5-7ю assertEquals проверили объект, который вернулся. И что? 1 тест. И следуя рекомендации 1 assert на 1 тест мы получаем кучу операторных скобок, 8 методов и 10 шагов в сторону от рекомендации - "тест как документация". Вот все было перед глазами, а теперь иди собирай все все мысли по классу.

И если ты не следуешь бездумно всем рекомендациям, описанным в инженерных книгах, то ты, так же как и я получишь один тест с 3-5-7ю ассертами. Но тут есть другой бок - скорее всего ты, так же как и я, утомился перезапускать этот один стройный тест. Да-да, интеграционный, блин, со спрингом под капотом, от чего он ранится не 10 милисекунд, а 100 секунд! Потому что мир джава жесток и беспощаден...
@SpringBootTest(classes = CodenjoyContestApplication.class,
        properties = "spring.main.allow-bean-definition-overriding=true")
@RunWith(SpringRunner.class)
@ActiveProfiles(SQLiteProfile.NAME)
@Import(RestGameControllerTest.ContextConfiguration.class)
@WebAppConfiguration
public class RestGameControllerTest {
...в этом тесте будет несколько assertEquals. И раз за разом, натыкаясь на очередной свалившийся ассерт, скорее всего ты, так же как и я, задавался вопросом: какого фига в junit тест валится после первого же ассерта?

Решение созрело.

Да, я знаю, что писать несколько assertEquals в одном тесте (было) не ок. Но писать по новой конструкции для каждого ассерта тоже перебор.
@Test
public void shouldBlahBlah_whenBlahBlah() {
   ...
}
Вот и написал свой assertEquals который агрегирует ошибки и валидирует их только, когда ты явно этого попросишь в @After методе
@After
public void checkErrors() {
    SmartAssert.checkResult();
}
В простом вариаенте запуска потребуется еще всего лишь импортнуть класс, в котором есть статический assertEquals метод.
import static com.codenjoy.dojo.stuff.SmartAssert.*;
А старый импорт удалить
import static org.junit.Assert.*;
А что умного? Так это вывод результатов. Каждый тест отработает от начала и до конца, не важно сколько assertEquals по дороге не пройдут. При этом ты увидишь в консоли все expected: <qwe> but was: <asd> блоки и кусочек стектрейса из тестового класса, в котором были вызовы измененного assertEquals длинной до 10 строк (на это можно повлиять).

Например вот тест, я в нем заведомо поломал первые 4 assert, заменив expected с true на false
@Test
public void shouldExists() {
    assertEquals(false, service.exists("first"));
    assertEquals("false", get("/rest/game/first/exists"));
   
    assertEquals(false, service.exists("second"));
    assertEquals("false", get("/rest/game/second/exists"));

    assertEquals(false, service.exists("non-exists"));
    assertEquals("false", get("/rest/game/non-exists/exists"));
}
В оригинальной версии тест прекратил бы выполнение после первой же строчки. Слетел ассерт - давай до свидания. Но не co SmartAssert
org.junit.ComparisonFailure: expected:<[fals]e>; but was:<[tru]e>
 com.codenjoy.dojo.web.rest.RestGameControllerTest.shouldExists(RestGameControllerTest.java:135)

org.junit.ComparisonFailure: expected:<[fals]e> but was:<[tru]e>
 com.codenjoy.dojo.web.rest.RestGameControllerTest.shouldExists(RestGameControllerTest.java:136)

org.junit.ComparisonFailure: expected:<[fals]e> but was:<[tru]e>
 com.codenjoy.dojo.web.rest.RestGameControllerTest.shouldExists(RestGameControllerTest.java:138)

org.junit.ComparisonFailure: expected:<[fals]e> but was:<[tru]e>
 com.codenjoy.dojo.web.rest.RestGameControllerTest.shouldExists(RestGameControllerTest.java:139)

java.lang.AssertionError: There are errors
 at org.junit.Assert.fail(Assert.java:88)
 at com.codenjoy.dojo.stuff.SmartAssert.checkResult(SmartAssert.java:118)
 at com.codenjoy.dojo.stuff.SmartAssert.checkResult(SmartAssert.java:132)
 at com.codenjoy.dojo.web.rest.RestGameControllerTest.checkErrors(RestGameControllerTest.java:98)
Важно, что ссылки на сами ассерты в коде в idea кликабельны.
(RestGameControllerTest.java:139)
Так же я добился замены базовым проверятором Idea констукции
expected:<[fals]e> but was:<[tru]e>
На такую, которая будет подсвечивать diff каждого слетевшего ассерта отдельно. В этом мне помог deprecated MultipleFailureException.

Код лежит тут. Лицензия GNU GPL v3. На здоровье.


пятница, 23 марта 2018 г.

Не все так просто с лицензиями

Пару интересных фактов о лицензиях. GNU GPL3 лицензия, которую можно отнести к тру лицензии свободного ПО. Но так просто нельзя установить эту лицензию себе в проект. А все потому, что ты скорее всего использовал компоненты, которые не совместимы с GPL3. Вот тут есть исчерпывающий список того с чем GPL3 совместима, а с чем нет. 

Не вдаваясь сильно глубоко в анализ не трудно найти что jUnit распространяется под Eclipse Public License 1.0 а это уже не совместимо с GPL3. Потому что "...в лицензии Eclipse удалены более широкие оговорки о прекращении действия в случае патентных преследований, направленных конкретно против тех, кто дорабатывал программу под лицензией Eclipse" 

Чё? Это из за этой какашулины предстоит выпилить весь jUnit из проекта. И я уверен, что это только цветочки. Может вообще оказаться, что на jdk писать свободный софт вообще не стоило. Взять хотя-бы эту шапку в каждом java классе. Раньше Sun хоть описывал лицензию, а тут прям к психотерапевту не ходи как эго выпирает.

/*
 * Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

Зачем столько отступов? И так все знают кто кого купил. И зачем маленькие компании вообще продаются большим? Чтобы большие стали еще больше, еще голоднее и причиняли всем больше добра? Что, если бы любая компания должна была быть величиной не больше 50 сотрудников. Вот такой вот антимонопольный всемирный закон прикрутить. Эгоподавляющий. И играйтесь с ним как хотите, но группами не больше чем 50 человек. Вспоминается игра http://agar.io где одни поглощают других и все только и заняты там, что догоняют и/или убегают.



А вот это вообще перл. JSON лицензия нам тоже не подойдет, потому что "Это лицензия первоначальной реализации формата обмена данными JSON. Эта лицензия принимает за основу Лицензию Expat, но добавляет пункт, требующий: “Программы должны использоваться для Добра, а не для Зла”. Это является ограничением на применение и, следовательно, противоречит свободе 0. Возможно, это ограничение не поддается защите в суде, но мы не можем заранее предполагать этого. Таким образом, лицензия несвободна."

Почему-то думаю, что инженер прикольнулся и хотел, чтобы его оригинальность оценили. А тут такое... 

Думается мне, что человечество пошло не тем путем. Все эти лицензии, патенты, законы - все как-то эго-оринтированно и людям не служит. Вернее служит, но не всем. 

Что важно знать про лицензии еще, так это то, что:

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

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

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

Будем внимательны!

А вот такие вот жизненные истории очень греют душу (их там много на ютьюбе - очень рекомендую)

пятница, 8 мая 2015 г.

Красивый assertThat в стиле Mockito

На ночь глядя взялся реализовать двусвязный список на Java. Пишу тест
    @Test
    public void testAddAtAtEnd() {
        List<String> list = get();

        list.add("1");
        assertEquals("[1]", list.toString());

        list.add("2");
        assertEquals("[1, 2]", list.toString());

        list.add("3");
        assertEquals("[1, 2, 3]", list.toString());

        list.add("4");
        assertEquals("[1, 2, 3, 4]", list.toString());
    }
Неуклюже и некрасиво! Хочу так!!
    @Test
    public void testAddAtAtEnd() {
        List<String> list = get();

        assertAfter("[1]", list).add("1");
        assertAfter("[1, 2]", list).add("2");
        assertAfter("[1, 2, 3]", list).add("3");
        assertAfter("[1, 2, 3, 4]", list).add("4");
        assertAfter("[1, 2, 3, 4, 5]", list).add("5");
        assertAfter("[1, 2, 3, 4, 5, 6]", list).add("6");
        assertAfter("[1, 2, 3, 4, 5, 6, 7]", list).add("7");
    }
Для этого мне надо вот
    private <T> T assertAfter(final String expected, final T object) {
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(),
                object.getClass().getInterfaces(),
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object invoke = method.invoke(object, args);
                        assertEquals(expected, object.toString());
                        return invoke;
                    }
                });
    }
Приятного!

пятница, 27 июня 2014 г.

[Java] Как перевести java тесты на groovy в maven проекте

Задолбался я писать тесты на java - жуть!

Чтобы перевести свои тесты на groovy надо

1) добавить зависимость в pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project ... >
    
 ...
 
 <dependencies>
        ...
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>2.35.0</version>
            <scope>text</scope>
        </dependency>
    </dependencies>
 
<build>
        ...

        <plugins>
            ...
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <executions>
                    <execution>
                        <id>test-compile</id>
                        <phase>test-compile</phase>
                        <configuration>
                            <tasks>
                                <mkdir dir="${basedir}/src/test/groovy"/>
                                <taskdef name="groovyc"
                                         classname="org.codehaus.groovy.ant.Groovyc">
                                    <classpath refid="maven.test.classpath"/>
                                </taskdef>
                                <mkdir dir="${project.build.testOutputDirectory}"/>
                                <groovyc destdir="${project.build.testOutputDirectory}"
                                         srcdir="${basedir}/src/test/groovy/" listfiles="true">
                                    <classpath refid="maven.test.classpath"/>
                                </groovyc>
                            </tasks>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    
</project>

2) создать в папке \src\test рядом с папкой java папку groovy

3) переместить туда все тесты, переименовав их в *.groovy

4) запустить и отладить (если надо)

Intelij Idea хорошо работает с goovy скриптами

четверг, 30 января 2014 г.

Хотелось выпендрится

Чем люблю проект выходного дня - тем, что тут я могу рефакторить без конца и придумывать новые комбинации строк кода, порой даже нелепые. Это все позволяет мне знать как можно, а как не стоит. Сейчас я предложу пример того как можно было бы, но не стоит :)

Пишу на ночь глядя эмулятор КР580ВМ80 - ну такое, just for fun. Конечно же по ТДД. Тесты рефакторю, как положено. И вот я в тесте пишу как-то так ассерт
    assertEquals("DEh", registry.m().toString());
При том, что
public class ProcessorTest {

    private Memory memory;
    private Processor processor;
    private Registry registry;

    @Before
    public void setup() {
        memory = mock(Memory.class);
        processor = new Processor(memory);
        registry = processor.registry();
    }
а интерфейс
public interface Registry {
    AByte b();
    AByte c();
    AByte d();
    AByte e();
    AByte h();
    AByte l();
    AByte m();
    AByte a();
}
а AByte содержит че-то умное и toString. Но мне хотелось выпендрится и сделать вот так в тесте, чтобы на at(). вываливались аутокомплитером IDE методы интерфейса Registry


Проверил, может ли такое Mockito. Оказалось может!
    private RegistryAssert assertValue(String data) {
        return new RegistryAssert(data);
    }
    
    class RegistryAssert {

        private String data;

        public RegistryAssert(String data) {
            this.data = data;
        }

        public Registry at() {
            // создаем шпиона для registry
            Registry spy = spy(registry); 
            // и следим за каждым его чихом
            setup(spy).b(); 
            setup(spy).c();
            setup(spy).d();
            setup(spy).e();
            setup(spy).h();
            setup(spy).l();
            setup(spy).m();
            setup(spy).a();
            return spy;
        }

        private Registry setup(Registry spy) {
            return doAnswer(new Answer<Object>() {
                @Override
                public Object answer(InvocationOnMock invocation) throws Throwable {
                    Object actual = invocation.callRealMethod();
                    assertEquals(data, actual.toString());
                    return actual;
                }
            }).when(spy);
        }
    }
Не делайте так ☺

среда, 16 октября 2013 г.

Шаблончик Spring MVC приложения с WebDriver в качестве тест фреймворка + Maven + Jetty

Вот недавно кодили с другом на тему ТДД веб приложение и конечно же первым делом, мы на час зависли на самом старте - начинать с нуля веб приложения на Java не так чтобы сложно, но муторно - с другими площадками попроще будет.

Так что предлагаю простой шаблончик Spring MVC приложения с WebDriver в качестве тест фреймворка и Maven в качестве резолвера зависимостей, который ранится на Jetty.


Все что он умеет делать - добавлять тудушки в список. Опять же - цели  написать полноценный TODO не стояло. Цель помочь тем, кто не знает с чего начать и хочет начать писать веб приложения на java. Этого думаю достаточно. Вот исходники. Чтобы не пропали.

Очень рекомендую IDEA - в нее стоит импортировать проект как Maven. запустить все тесты - они должны пройти. После целью jetty:run можно запустить приложение.


Дальше уже сами...

воскресенье, 20 января 2013 г.

#10 Пишем Web проект на Java в Eclipse для Tomcat. Билдим Ant. Проверяем Hudson. Тестим jUnit + EasyMock + jWebUnit. Коммитим в Svn.

Привет! Линк на прошлую серию тут. А сегодня история, которую мы реализуем, звучит так: "Появляется возможность работать с несколькими экзаменами - на странице выбора экзаменов теперь отображается список." Я ее разбил на три составляющие, потому что по моей предварительной оценке она занимает слишком много времени. Все что сложнее логики логинки более чем в 4 раза - все стоит разбивать на подзадачи.

Подзадачи я вижу такие: 
1. Теперь о юзере может храниться более сложная информация а не просто пароль/сдал/не сдал. Пускай это будет тот же properties файл, но немного другого формата.
2. Изменение части контроллера - надо научить его понимать какой экзамен пользователь выбрал и от этого подгружать необходимую xml с экзаменом. Тут же решение задачи, что название экзамена != названию xml файла с экзаменом.
3. Получение списка всех экзаменов по существующим xml файлам. Тут же подгрузка только тех экзаменов, которые студент еще не прошел.

Начнем с первой.

Изменим внутреннюю структуру файла так, чтобы была возможность содержать дополнительную информацию о пользователе, но так, чтобы ничего кроме UserService не изменилось.

Сейчас в файле с описанием пользователей информация хранится в таком формате <имя пользователя>=<пароль>|"FAILED"|"PASSED"


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


Но для начала запустим все тесты:


Все зелено? - продолжаем...

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


Так же я хочу превратить функциональный тест UserServiceFileTest в контрактный, завязанный только на интерфейс UserService. Сейчас в этом классе содержится логика тестирования интерфейса UserService и логика подготовки тестовых данных.


Разделим их - логику тестирования интерфейса - в абстрактный класс, а логику подготовки тестовых данных - в наследника этого абстрактного класса.

Для разделения, выделим все тесты которые в своем теле используют поле users и попробуем применить к ним рефакторинг PullUp.



У нас не получилось это сделать потому, что у исходного класса нет родителя, куда бы IDE мог бы переместить выделенные методы. Создадим этого родителя.





А после снова выделим все тесты, использующие поле users, из наследника новоиспеченного UserServiceTest и применим к ним рефакторинг PullUp.


Выделение нам особо не помогло, а потому выделим все методы/константы/поля и сними выделение с тех, которые должны остаться в наследнике - тех, которые отвечают за подготовку и формат файла с пользователями. И нажмем Finish.


Потом еще раз Next.


Подождем, это займет некоторое время


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


Проглядываем что произошло в результате рефакторинга (может чего пошло не по плану?) и ждем Finish


В результате содержимое исходного класса разделится по двум.

Конкретика относительно структуры файла останется в наследнике


А все, что зависит от интерфейса UserService - уйдет в абстрактного родителя. + Родитель компилировать не будет, потому что там нет никакого упоминания о users поле.


Исправим это, добавив в абстрактного родителя новый абстрактный метод getUsers конфигурируемый списком пользователей и их паролями. Ну и родителя которого мы называем абстрактным - сделаем наконец-то абстрактным.


Это нам слабо помогло избавиться от ошибок компиляции, но минутку терпения. Добавим теперь метод getDefaultUsers и заменим во всем классе users на вызов этого метода, но ВНИМАНИЕ!, только в тех тестах, которые users используют один раз. В тех тестах, которые используют более одного раза users надо добавить одноименную локальную переменную, значение которой получить вызвав тот же метод getDefaultUsers.


Замечу, что default данные мы украли из наследника


Как результат большинство ошибок компиляции устранено - не компилируются пока только всего два теста, использующие метод, которые остался в наследнике.


Добавим его тут как абстрактный.


С абстрактным родителем можно закончить - в результате у нас получился тест, который зависит только от UserService - его как мы уже знаем называют контрактным.


Осталась разобраться с наследником-конкретикой. В нем как минимум две ошибки компиляции.


IDE нам поможет. Вначале нам надо модифицировать метод assertThatUserPassed так, чтобы он стал реализацией одноименного абстрактного метода из родителя.

Первый шаг - убрать ключевое слово static.


Следующий шаг - сделать методы похожими по типу прокидываемых исключительных ситуаций.


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


Теперь попросил в наследнике дореализовать недостающие методы.


Итак почти все готово, осталось только доделать getUsers метод


Раньше эту роль выполняли initUserService и makeUsersFile методы вместе. Перенесем их реализацию в getUsers


Теперь надо снова разобраться с прокидываемыми исключениями.



После, как и в прошлый раз - обобщим в абстрактном родителе все до прокидывания Exception и попросим абсолютно все тесты так же прокидывать Exception.


В ходе добавления я заметил, что метод assertExceptionWithMessage кажется не на своем месте, т.е. ему тут не место. Перенесем его в какой-то TestUtils. Для этого создадим пустой класс в пакете test/utils.


Мы могли бы вручную его перенести, но это потребует много ручной работы, а мне лень ее делать. Потому что за ручным переносом:


последовало бы ручное изменение всех вызовов с учетом нового местоположения. Фуу.. А если ошибку какую-то внесу? Я и так долго тесты не запускал...

Итак попросим IDE cделать перенос за нас. Поставим курсор ввода на название метода в исходном классе и выполним рефакторинг Move (горячая клавиша Alt-Shift-V или меню Refactoring->Move...).

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


Что мы можем сделать, кроме как сдаться и сделать все руками? Идея пришла сразу - надо сделать его статическим и публичным.


А после повторить Рефакторинг Move. Теперь IDE нам предлагает варианты - укажем класс, куда хотим перенести метод и посмотрим от чего IDE нас избавило





IDE даже в разделе import порядок навела. Супер! Вот, что мне было делать лень. Жмем OK и продолжаем


Следующая трудность не такая простая.


Дело в том, что мы прокидывали везде Exception, а для тестирования исключительных ситуаций используем анонимную реализацию стандартного Java интерфейса Runnable - в его сигнатуре не указано throws Exception и поменять мы его не можем.


Но мы можем написать свой! Сделаем это в том же TestUtils классе.


Теперь для каждого теста достаточно будет сделать так


Но вручную я сделаю это всего раз (ленив, помните?) - все остальное я попрошу сделать IDE используя замену по файлу.


Риcунок кишит пометками, потому я его поясню. Дело в том, что в разработке пользоваться мышкой - это все равно что спринтеру привязать к ногам 10кг свинцовые гири и попросить пробежать дистанцию на время. По этой причине везже где только можно я пользуюсь горячими главишами. В принципе мышка нужна только для того, чтобы добраться в соответствующее меню и подглядеть какая там стоит клавиша, а после юзать только ее. Replace All по всему файлу я использую довольно часто, а потому давно делаю это без участия мышки.

Всего в этом процессе 6 этапов:
1) Выделяем строчку, которую только что исправили ручками. Но не мышкой выделяем а, поставив курсор в начало строки нажатием Home с последующтми нажатием Shift-End.
2) Копируем строчку в буфер обмена нажатием Ctrl-C (или Ctril-Ins кто как привык)
3) Перемещаемся к следующему методу, содержащему заменяемую строчку нажатием Ctrl-Shift-Down, а потом к самой строчке (Down, Down, Down)
4) Выделяем и это строчку нажатием Home, Shift-End.
5) Вызываем диалог Поиска/Замены нажатием Ctrl-F
6) Выделенный текст автоматически скопируется в поле Find (собственно за этим его и выделяли на шаге 4) а мы нажмем Tab, чтобы проскочить это поле
7) Вставим ранее скопированную строчку-заменитель нажатием Ctrl-V (или Shift-Ins кому как удобнее)
8) Нажмем на клавишу Replace All? но НЕ МЫШКОЙ! :) а подглянув, какая буква в надписи на кнопке подсвечена и нажав на нее с Alt кнопкой (в данном случае Alt-A)

Итого комбинация Home, Shift-End, Ctril-Ins, Ctrl-Shift-Down, Down, Down, Down, Home, Shift-End, Ctrl-F, Tab, Shift-Ins, Alt-A

Длиннющая! Но запоминать ее всю не надо, дело в том, что регулярно пользуясь горячими клавишами вскоре это войдет в привычку и ты не будешь задумываться о том, что нажать, а всего лишь будешь думать о том, что делать, а руки, в свою очередь, сами нажмут то, что надо. Если сделать горячие клавиши ежедневной привычкой, то вскоре окружающие будут удивляться как молниеносно ты работаешь - и это проверенный факт. У меня процесс замены с горячими клавишами занял 4 секунды. Если бы со мной кто-то сейчас сидел в паре, то вряд ли бы он понял, что я сделал - хотя бы потому, что диалоговое окно поиска/замены мелькнуло на секунду и сразу пропало. Кстати с мышкой тот же процесс занял 24 секунды.

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

Вот она, вредная, зависимость


Можно исправить код и оставить зависимость, но я предпочитаю устранить ее. Вопрос в том, как?

Для начала закрою все public методы, сделав их видимыми только в пакете - именно это стало источником лишних связей.


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

Вот вторая бяка!


Ну да, для работы функциональных тестов, надо чтобы кто-то перед каждым тестом заполнял файл с пользователями исходными данными. Кто как не тест, который разбирается в дебрях UserServiceFileImpl.

Ошибка тут только одна - наличие функциональных тестов в проекте. Этим тестам всегда надо знать все и вся.

Кстати, наше превращение простого теста в контрактный не сделало его модульным. UserServiceFileTest по прежнему тестирует UserServiceFileImpl вместе с Properties классом и файловой системой. Функциональный тест, косящий под модульный - интересно звучит.


Запущу ка я его, чтобы проверить как прошло выделение контрактного теста.

Некоторые классы в проекте не компилируются, об чем сообщит мне IDE но я попрошу ее проигнорить это.



Зеленый? Это хорошо, но это намекает на то, что надо сделать commit. А потому я вредные зависимости на время оставлю в покое, то есть верну public на место и статический доступ заменю еще более некрасивым динамическим.


Фуу... Теперь видно невооруженным взглядом видно всю некрасивость этой зависимости.

Пока я разоблачением занимался, увидел, провтык в контрактном тесте. Дело в том, что нет связи между теми пользователями, которые передаются из абстрактного контрактного теста в наследник-конкретику и теми, которые будут сохранены в файл. Это станет заметно если убить константу data.


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


Но это опять поломает нашего вездесущего клиента


Выхода пока нет - передадим ему исходные данные (их я скопипастил из контрактного теста).


Теперь, пока весь проект компилится - я могу запустить все тесты.


Все зеленое? Сохраняемся. А то, что стоит сделать дальше - записано в TODO: устранить вредную зависимость. Вернемся к ней, когда будет настроение.



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

Если разделить эту задачу на две:
1) изменение формата, но экзамен остается один
2) добавление возможности сохранять информацию о нескольких экзаменах для каждого юзера.
то мы сможем реализовать первую, без добавления новых тестов, т.к. задача по сути - рефакторинг, а вот вторая - это уже расширение функциональности - а это, в свою очередь, требует написания теста-требования перед изменением в коде (помнишь, договаривались? Утром требование, вечером код!)

Вперед! Вот исходная версия UserServiceFileImpl. Только что в ней я заметил одну маленькую недоработку, которую тут же пофиксил.



Тесты не повалились, а потому я закомичу эту правку.



Итак что этот класс делает? При вызове любого его метода класс грузит данные из файла в локальную переменную Properties, после работает с ней и в конце сохраняет обратно в файл. Свойством класса является только путь к файлу. Как-то это не по ООП. Класс в ООП работающий с данными должен инкапсулировать сами данные, а не ссылку на то место где они находятся.

Что если из UserServiceFile сделать singleton а UserFactory будет управлять его созданием. Загрузка из файла будет во время инстанциирования в конструкторе UserServiceFile, а сохранение данных в файл всякий раз, когда это понадобится. Неее...

А что, если мы разделим класс UserServiceFile на два класса - первый будет работать с данными о пользователях инкапсулированных вместе с методами загрузки в файл во втором классе. А еще я узнал, что Properties это реализация Map. И что, если теперь полем класса будет этот самый Map, и лишь на момент загрузки/сохранения будет известно что это Porperties?

А может попробовать наконец-то создать класс User, инкапсулирующий в себе все свойства пользователя, UserService будет хранить в себе Map этих User данных, а третий класс, допустим UsersSaver будет сохранять Map в файл любыми доступными средствами. UserService знает про User и UsersSaver. Не сложно ли? А попробуем!

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

Для создания класса есть хорошая горячая клавиша: Alt-Shift-N,C ;)



Наполним его смыслом. Тут будем писать код так, как будь-то бы он уже существует. Компилятор будет ругаться, и мы попросим IDE создать все необходимое за нас.


Много ошибок! Создадим для начала интерфейс User



Продолжение следует…