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


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

вторник, 10 января 2012 г.

Java for fun: Assert That по принципу FEST

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



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


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

Допустим, есть у нас список мероприятий

package dom;

import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class EventList implements Iterable<Event> {
    private List<Event> events = new LinkedList<Event>();

    public EventList(Event...events) {
        this.events.addAll(Arrays.asList(events));
    }

    public void add(Event event) {
        this.events.add(event);
    }

    public Iterator<Event> iterator() {
        return events.iterator();
    }
}

Список состоит из конкретных мероприятий

package dom;

import java.util.Iterator;

public class Event {
    private String name;
    private Place place;
    private String date;
    private ParticipantList participants = new ParticipantList();

    public Event(String description, String date, Place place) {
        this.name = description;
        this.place = place;
        this.date = date;
    }

    public String getName() {
        return name;
    }

    public Place getPlace() {
        return place;
    }

    public String getDate() {
        return date;
    }

    public Iterator<Participant> participantsIterator() {
        return participants.iterator();
    }

    public void addParticipatns(Participant... participants) {
        for (Participant participant:participants) {
            this.participants.add(participant);
        }
    }
}

Мероприятие состоит так же их списка участников

package dom;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class ParticipantList implements Iterable<Participant> {

    private List<Participant> participants = new LinkedList<Participant>();

    public void add(Participant participant) {
        this.participants.add(participant);
    }

    public Iterator<Participant> iterator() {
        return participants.iterator();
    }
}

А вот и описание участника

package dom;

public class Participant {

    private String name;
    private String email;

    public Participant(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

Так же мероприятие содержит ссылку на место проведения

package dom;

public class Place {

    private String address;
    private String details;

    public Place(String address, String details) {
        this.address = address;
        this.details = details;
    }

    public String getAddress() {
        return address;
    }

    public String getDetails() {
        return details;
    }
}

Вот пример классического теста

import static assertions.DomAssertions.assertThat;
import static org.junit.Assert.assertTrue;

import dom.Event;
import dom.EventList;
import dom.Participant;
import dom.Place;
import org.junit.Before;
import org.junit.Test;

import java.util.Iterator;

public class TestTest {

    private Participant vasia;
    private Participant petia;
    private Participant masha;
    private Place vasiaHome;
    private Event vasiaBitrthday;
    private Event pairProgramming;
    private Place office;
    private EventList events;

    @Before
    public void setup(){
        petia = new Participant("Петя Васечкин", "petia@gmail.com");
        masha = new Participant("Маша Пупкина", "pupkina@mail.ru");
        vasia = new Participant("Вася Пупкин", "super@mail.ru");

        vasiaHome = new Place("Киев, ул. Лабораторная 12/34е", "На домофоне 123");
        office = new Place("Киев, ул. Охотничья 43/123",
                "На рисепшене охраннику скажешь, что пришел ко мне");

        vasiaBitrthday = new Event("День рождения Васи", "2012-01-10", vasiaHome);
        pairProgramming = new Event("Собрались попилить вместе проектик",
                "2012-01-15", office);

        vasiaBitrthday.addParticipatns(vasia, petia, masha);
        pairProgramming.addParticipatns(petia, vasia);
        events = new EventList(vasiaBitrthday, pairProgramming);
    }

    @Test
    public void classicTest(){
        assertContainsParticipantWithEmail(events, "pupkina@mail.ru");
    }

    private void assertContainsParticipantWithEmail(EventList events, String email) {
        boolean found = false;
        for (Event event : events) {
            Iterator<Participant> participants = event.participantsIterator();
            while (participants.hasNext()) {
                if (participants.next().getEmail().equals(email)) {
                    found = true;
                }
            }
        }

        assertTrue(String.format("email '%s' не найден", email), found);
    }
}

Кто не писал такие пользовательские ассерты? Я писал... :)

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

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

Какие альтернативы?

Представь, что можно написать такой тест:

@Test
    public void assertThatTest(){
        assertThat(events).hasEvent().withName("День рождения Васи");
        assertThat(events).hasParticipant().withEmail("pupkina@mail.ru");
        assertThat(events).hasPlace().withAddress("Киев, ул. Лабораторная 12/34е");
        
        assertThat(vasiaBitrthday).withName("День рождения Васи");
        assertThat(pairProgramming).withDate("2012-01-15");
        assertThat(pairProgramming).hasParticipant().withEmail("petia@gmail.com");
        assertThat(vasiaBitrthday).hashPlace().withDetails("На домофоне 123");

        assertThat(vasiaHome).withAddress("Киев, ул. Лабораторная 12/34е");

        assertThat(vasia).withEmail("super@mail.ru");
        assertThat(vasia).withName("Вася Пупкин");
    }

Удобно? Мне да...

Как создать такой assert-фреймворк? Идея проста. Для начала стоит написать assertThat(someObject).somecheck(someExpected) и реализовать метод assertThat. Вот пример для моих объектов модели.

package assertions;

import dom.Event;
import dom.EventList;
import dom.Participant;
import dom.Place;

import java.util.Arrays;
import java.util.List;

public class DomAssertions {

    public static EventListAssertion assertThat(EventList events) {
        return new EventListAssertion(events);
    }

    public static EventAssertion assertThat(List<Event> events) {
        return new EventAssertion(events);
    }

    public static EventAssertion assertThat(Event event) {
        return new EventAssertion(Arrays.asList(new Event[]{event}));
    }

    public static ParticipantAssertion assertThat(Participant participant) {
        return new ParticipantAssertion(participant);
    }

    public static PlaceAssertion assertThat(Place place) {
        return new PlaceAssertion(place);
    }
}

Так получается, что на каждый класс проверяемых объектов надо написать свой ClassAssertion.

А вот реализация всех проверяльщиков содржит уже методы проверки.

package assertions;

import collectors.Collector;
import dom.Event;
import dom.Place;
import org.fest.assertions.Assertions;

import java.util.Arrays;
import java.util.List;

public class PlaceAssertion {

    private List<Place> places;

    public PlaceAssertion(List<Event> events) {
         // это так же мой самописный колектор, который ходит по спискам 
         // и собирает подколлекции, онем чуть позже. 
         // Сейчас достаточно знать, что в результате селекнутся все места
         // описанные в списке мероприятий
         places = Collector.fromEvents(events).places().list();
    }

    public PlaceAssertion(Place place) {
        places = Arrays.asList(place);
    }

    public void withDetails(String details) {
        // Я тут воспользовался родным FESTAssertion assertThat 
        // методом для работы с коллекцией объектов. 
        // Зачем изобретать велосипед?
        Assertions.assertThat(places)
                .onProperty("details")
                .contains(details);
    }

    public void withAddress(String address) {
        Assertions.assertThat(places)
                .onProperty("address")
                .contains(address);
    }
}

Аналогично и для другого ассертора :)

package assertions;

import collectors.Collector;
import dom.Event;
import dom.Participant;
import org.fest.assertions.Assertions;

import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.fail;

public class ParticipantAssertion {
    private List<Participant> participants;

    public ParticipantAssertion(List<Event> events) {
        participants = Collector.fromEvents(events).participants().list();
    }

    public ParticipantAssertion(Participant participant) {
        this.participants = Arrays.asList(participant);
    }

    public void withEmail(String email) {
        Assertions.assertThat(participants)
                .onProperty("email")
                .contains(email);
    }

    public void withName(String name) {
        Assertions.assertThat(participants)
                .onProperty("name")
                .contains(name);
    }
}

И третьего...

package assertions;

import dom.Event;
import org.fest.assertions.Assertions;

import java.util.List;

public class EventAssertion {
    private List<Event> events;

    public EventAssertion(List<Event> events) {
        this.events = events;
    }

    public void withName(String name) {
        Assertions.assertThat(events)
                .onProperty("name")
                .contains(name);
    }

    public void withDate(String date) {
        Assertions.assertThat(events)
            .onProperty("date")
            .contains(date);
    }

    // тут можно увидеть странные методы
    // но они нужны для того, чтобы можно было сделать так
    // assertThat(events).hasEvent().hasParticipant().withEmail("pupkina@mail.ru");
    public ParticipantAssertion hasParticipant() {
        return new ParticipantAssertion(events);
    }

    public PlaceAssertion hashPlace() {
        return new PlaceAssertion(events);
    }

}

И еще один

package assertions;

import dom.Event;
import dom.EventList;

import java.util.LinkedList;
import java.util.List;

public class EventListAssertion {
    private List<Event> events;

    public EventListAssertion(EventList events) {
        this.events = asList(events);
    }

    public EventAssertion hasEvent() {
        return new EventAssertion(events);
    }

    // Это чудо для перегонки Iterable в List. Чую, велосипед :)
    private <T> List<T> asList(Iterable<T> iterable) {
        List<T> result = new LinkedList<T>();
        for (T t : iterable) {
            result.add(t);
        }
        return result;
    }

    public ParticipantAssertion hasParticipant() {
        return new ParticipantAssertion(events);
    }

    public PlaceAssertion hasPlace() {
        return new PlaceAssertion(events);
    }
}

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

package collectors;

import dom.Event;

import java.util.List;

public class Collector {

    private List<Event> events;

    public Collector(List<Event> events) {
        this.events = events;
    }

    public static Collector fromEvents(List<Event> events) {
        return new Collector(events);
    }

    public ParticipantCollector participants() {
        return new ParticipantCollector().fromEvents(events);
    }

    public PlaceCollector places() {
        return new PlaceCollector().fromEvents(events);
    }
}

И два сборщика

package collectors;

import dom.Event;
import dom.Place;

import java.util.LinkedList;
import java.util.List;

public class PlaceCollector {
    private List<Place> places = new LinkedList<Place>();

    PlaceCollector fromEvents(List<Event> events) {
        for (Event event : events) {
            fromEvent(event);
        }
        return this;
    }

    PlaceCollector fromEvent(Event event) {
        places.add(event.getPlace());
        return this;
    }

    public List<Place> list() {
        return new LinkedList<Place>(places);
    }
}

И

package collectors;

import dom.Event;
import dom.Participant;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class ParticipantCollector {
    private List<Participant> participants = new LinkedList<Participant>();

    ParticipantCollector fromEvents(List<Event> events) {
        for (Event event : events) {
            Iterator<Participant> participants = event.participantsIterator();
            while (participants.hasNext()) {
                this.participants.add(participants.next());
            }
        }
        return this;
    }

    public List<Participant> list() {
        return new LinkedList<Participant>(participants);
    }
}

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

А вот и исходники (Maven проект, но на всякий либа FEST Assertions внутри).

Приятного аппетита!

---------------------------------------

Хорошая мысля приходит опосля :)

Теперь если немного переделать void методы в ассерторах

...
public class PlaceAssertion {

    ... 

    public void withDetails(String details) {
        Assertions.assertThat(places)
                .onProperty("details")
                .contains(details);
    }

    public void withAddress(String address) {
        Assertions.assertThat(places)
                .onProperty("address")
                .contains(address);
    }
}

на

...
public class PlaceAssertion {

    ... 

    public PlaceAssertion withDetails(String details) {
        Assertions.assertThat(places)
                .onProperty("details")
                .contains(details);
        return this;
    }

    public PlaceAssertion withAddress(String address) {
        Assertions.assertThat(places)
                .onProperty("address")
                .contains(address);
        return this;
    }
}

то можно написать так :)

assertThat(events).hasEvent().hashPlace()
            .withAddress("Киев, ул. Охотничья 43/123")
            .withDetails("На рисепшене охраннику скажешь, что пришел ко мне");

Но это не совсем гуд, потому как по тексту кажется что условия проверки места по адресу и описанию объединенные в AND, а на практике можно написать

assertThat(events).hasEvent().hashPlace()
            .withAddress("Киев, ул. Охотничья 43/123") // это с одного Place
            .withDetails("На домофоне 123"); // a это с другого Place

и такой assert тоже пройдет, хотя строки - кишки разынх Place объектов.

Выход? Есть. Еще немного усложняем :)

package assertions;

import collectors.Collector;
import dom.Event;
import dom.Place;
import org.fest.assertions.Assertions;

import java.nio.channels.Selector;
import java.util.Arrays;
import java.util.List;

public class PlaceAssertion {

    protected List<Place> places;

    public PlaceAssertion(List<Event> events) {
        places = Collector.fromEvents(events).places().list();
    }

    public PlaceAssertion(Place place) {
        places = Arrays.asList(place);
    }

    public PlaceSubAssertion withDetails(String details) {
        // проверяем, что соблюдается первое условие
        new PlaceSubAssertion(places).andDetails(details);

        // выделяем всех найденных по этому условию
        List<Place> subList = Collector.fromPlaces(places).withDetails(details).list();

        // даем возможность првоерить второе условие клиенту
        return new PlaceSubAssertion(subList);
    }

    public PlaceSubAssertion withAddress(String address) {
        new PlaceSubAssertion(places).andAddress(address);

        List<Place> subList = Collector.fromPlaces(places).withAddress(address).list();
        return new PlaceSubAssertion(subList);
    }
}

А остальное в другом классе :)

package assertions;

import dom.Place;
import org.fest.assertions.Assertions;

import java.util.List;

public class PlaceSubAssertion {
    private List<Place> places;

    public PlaceSubAssertion(List<Place> places) {
        this.places = places;
    }

    public void andDetails(String details) {
        Assertions.assertThat(places)
                .onProperty("details")
                .contains(details);
    }

    public void andAddress(String address) {
        Assertions.assertThat(places)
                .onProperty("address")
                .contains(address);
    }
}

Вот как то так :) Еще сложнее, зато можно написать теперь так:

assertThat(events).hasEvent().hashPlace()
            .withAddress("Киев, ул. Охотничья 43/123")
            .andDetails("На рисепшене охраннику скажешь, что пришел ко мне");

И тут уже валидация будет так доктор прописал...

Если интересно посмотреть на исходники второго варианта - они тут.

Цель этого поста не

Надеюсь продолжение следует...

1 комментарий: