t.me/atinfo_chat Telegram группа по автоматизации тестирования

Конвертировть cucumber data table в список объектов кастомного типа. Объект содержит поле типа Collection

cucumber
Теги: #<Tag:0x00007f9c5504b980>

(Vital) #1

Есть cucumber сценарий со следующим степом:

And I add a new user
| firstName | lastName | workEmail | workPhone | userName | assignedRoles | assignedAdvisorCodes |
| Steven | Gerrard | steeveg@test.com | 12312312346345 | steeveg | Advisor,Compliance | 1107,1108 |

при этом Step definition:

@And("^I add a new user$")
public void i_add_a_new_user(List<User> users)

а также часть User POJO:

public class User {
    private Set<String> assignedRoles;
    private Set<String> assignedAdvisorCodes;

Затык в том, что у User есть поля типа Set. Соответственно, они не заполняются, когда из data table генерится user. С примитивными и ссылочными типами никаких проблем нет, затык лишь в том, как заставить у объекта User заполнятся поля типа Set(в принципе, это может быть любой вид коллекции).


(Valentin G ) #2

Вопрос немного не по теме, а что, можно как параметр сделать кастомный класс (если без сетов), и он получит все правильные данные из таблицы?


(Vasiliy Rakshin) #3

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


(Denis Veselovskiy) #4

А так не работает?

@When("^I add a new user$")
public void I_Enter_My_Regular_Expenses(DataTable dataTable) {
  List<User> usersList = dataTable.asList(User.class);
}

(Павел) #5

Как раз задавался вопросом, каким образом надежно и универсально парсить таблицу в DTO. Вцелом я вижу 2 способа решать подобную задачу:

  1. (ортодоксальный) Определять параметр definition метода как DataTable, либо как Map<,> (List<Map<,>> для коллекции) и конвертировать в индивидуальном порядке.

  2. (молодежный, Cucumber 3+) Конвертировать таблицу в POJO автоматически через TypeRegistryConfigurer.

В варианте №1 все понятно - с помощью оператора new, геттеров/сеттеров мы парсим таблицу/мапу в объект. Можно даже прикрутить какой-то BeanUtils (apache или spring) чтобы сократить бойлерплейт. Такой способ работает если классов несколько, иначе плохо масштабируется.

В варианте №2 нужно использовать что-то, что умеет конвертировать мапу в объект, например Jackson. Теперь осталось только определить, как jackson’ом делать конвертацию String → Map.Entry.Value → поле DTO для случая коллекций. К сожалению, я не нашел чисто jackson’овского решения, поэтому String → Map.Entry.Value придется делать своими силами.

Итак, решение следующее:

  1. Закидываем класс ParameterTypes из ссылки выше куда-то в glue package.
  2. Решаем, каким образом мы будем писать коллекции в ячейке таблицы в feature файле (чтобы потом правильно её парсить). Пускай формат будет типа [val1, val2] (квадратные скобки, разделитель запятая и 0-1 пробелов)
  3. в ParameterTypes меняем имплементацию метода
public <T> T transform(Map<String, String> map, Class<T> aClass, TableCellByTypeTransformer tableCellByTypeTransformer)

примерно на такую:

        @Override
        public <T> T transform(Map<String, String> map, Class<T> aClass, TableCellByTypeTransformer tableCellByTypeTransformer) {
            // обходим мапу, чтобы сконвертировать ячейки
            Map<String, Object> converted = map.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> {
                var value = e.getValue();
                if (StringUtils.startsWith(value, "[") && StringUtils.endsWith(value, "]")) {
                    String collectionStr = StringUtils.strip(value, "[]");
                    return StringUtils.split(collectionStr, ", ");
                }
                return value;
            }));
            try {
                String json = objectMapper.writeValueAsString(converted);
                return objectMapper.readerFor(aClass).readValue(json);
            } catch (IOException ex) {
                throw new IllegalArgumentException("Failed to convert map to DTO", ex);
            }
        }
  1. Вот и все. Осталось протестить:
@lombok.Data
public class User {
    private String firstName;
    private String lastName;
    private String workEmail;
    private String workPhone;
    private String userName;
    private Set<String> assignedRoles;
    private Set<String> assignedAdvisorCodes;
}
  Scenario: print users
    Given I have next users:
      | firstName | lastName | workEmail        | workPhone      | userName | assignedRoles         | assignedAdvisorCodes |
      | Steven    | Gerrard  | steeveg@test.com | 12312312346345 | steeveg  | [Advisor, Compliance] | [1107,1108]          |
    @Given("I have next users:")
    public void printUsers(List<User> users) {
        log.info("Users received: {}", users);
    }
[INFO ] 11:40:38: Users received: [User(firstName=Steven, lastName=Gerrard, workEmail=steeveg@test.com, workPhone=12312312346345, userName=steeveg, assignedRoles=[Compliance, Advisor], assignedAdvisorCodes=[1107, 1108])]




Примечания:

  1. Если в таблице всегда 1 строка, то в параметре definition метода вместо List<User> можно оставить просто User - будет работать и так.
  2. Иногда есть необходимость передавать через таблицу сложные DTO. Например, у вас есть такое:
@Data
public class Organization {
    private User manager;
    private List<User> employees;
}

Решение следующее - записывать такое DTO как плоский JSON. Все что нужно сделать - подключить в проект библиотеку, например, json-flattener, и при конвертации таблицы вместо этого:

String json = objectMapper.writeValueAsString(converted);

Использовать это:

String json = JsonUnflattener.unflatten(objectMapper.writeValueAsString(converted));

Тогда в таблице мы можем записать наш DTO как плоский JSON:

  Scenario: print organization
    Given I have next organization:
      | manager.firstName | manager.lastName | employees[0].userName | employees[1].userName |
      | Steve             | Jobs             | Bobby                 | Dobby                 |
    @Given("I have next organization:")
    public void printOrganization(Organization organization) {
        log.info("Organization received: {}", organization);
    }
[INFO ] 11:50:53: Organization received: Organization(manager=User(firstName=Steve, lastName=Jobs, workEmail=null, workPhone=null, userName=null, assignedRoles=null, assignedAdvisorCodes=null), employees=[User(firstName=null, lastName=null, workEmail=null, workPhone=null, userName=Bobby, assignedRoles=null, assignedAdvisorCodes=null), User(firstName=null, lastName=null, workEmail=null, workPhone=null, userName=Dobby, assignedRoles=null, assignedAdvisorCodes=null)])