-Поиск по дневнику

Поиск сообщений в rss_forum_sources_ru

 -Подписка по e-mail

 

 -Постоянные читатели

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 29.07.2007
Записей:
Комментариев:
Написано: 80


TDD vs не TDD

Среда, 02 Сентября 2020 г. 22:30 + в цитатник
korvin:
Цитата D_KEY @
а в реальном декларативном коде не вознкает потребности в рефакторинге?

Так я ж бомблю, что его почти не пишут, этот самый декларативный код, предпочитая обмазаться тестами и говнякать =)

Добавлено
Цитата D_KEY @
А хорошо получалось или было все нестабильно?

Всё было плохо.

Цитата D_KEY @
А, то есть мы полностью переписываем код каждый раз?

Зачем каждый раз? Если ты про то, чтобы переиспользовать эту функцию в новой фиче, то если она подходит, можно и переиспользовать, но тогда две фичи будут от неё зависеть и менять её нельзя. Да и зачем? Если у тебя новые требование (новая формула, скажем, умножать нужно не на два, а на три), то естественно нужно написать новую функцию и использовать там, где нужно, а не трогать старую. Ты что, open-close принцип забыл?

Цитата D_KEY @
То, что если он недекларативен ради оптимизации, то тогда тесты имеет смысл писать?

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

Цитата D_KEY @
А что, декларативный код не оптимизируют?

А что такое «оптимизация декларативного кода»?

Добавлено
К слову о моках и в чём с ними проблема.

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

    public final class Transfer {
    public static final class Request {
    public final Account.ID source;
    public final Account.ID destination;
    public final Money amount;
    public Request(Account.ID source, Account.ID destination, Money amount) {
    this.source = source;
    this.destination = destination;
    this.amount = amount;
    }
    }
    public static final class InsufficientFunds extends RuntimeException {}
    private final Accounts accounts;
    private final Compliance compliance;
    private final Transactions transactions;
    private final Notifier notifier;
    public Transfer(Accounts accounts, Compliance compliance, Transactions transactions, Notifier notifier) {
    this.accounts = accounts;
    this.compliance = compliance;
    this.transactions = transactions;
    this.notifier = notifier;
    }
    public void submit(Request r) {
    final var source = accounts.load(r.source);
    final var destination = accounts.load(r.destination);
    validate(source, destination, r.amount);
    final var transaction = Transaction.fresh(r.source, r.destination, r.amount);
    transactions.store(transaction);
    notifyUsers(source, destination, transaction.id);
    }
    private void validate(Account source, Account destination, Money amount) {
    checkCompliance(source, destination, amount);
    checkBalance(source, amount);
    }
    private void checkCompliance(Account source, Account destination, Money amount) {
    if (source.hasSameOwnerAs(destination)) {
    return;
    }
    compliance.validateTransfer(source.owner, destination.owner, amount);
    }
    private void checkBalance(Account source, Money amount) {
    if (source.canWithdraw(amount)) {
    return;
    }
    throw new InsufficientFunds();
    }
    private void notifyUsers(Account source, Account destination, Transaction.ID transaction) {
    notifier.notify(source.owner, transaction, "withdraw");
    notifier.notify(destination.owner, transaction, "top up");
    }
    }


у него четыре внешние зависимости, вот одна из них:
    public interface Accounts {
    Account load(Account.ID id);
    // more methods
    // ...
    }

три другие примерно такие же: интерфейсы с разными методами, я описал только те, что используются в сервисе Transfer.

теперь для юнит-тестов мы мокаем эти интерфейсы, как обычно это делают, что-то вроде
    final var accounts = Mockito.mock(Accounts);
    ...
    final var sourceAccount = // new account
    ...
    when(accounts.load(sourceAccount.id)).thenReturn(sourceAccount);
    ...
    subject.submit(new Transfer.Request(sourceAccount.id, ...));
    ...
    // assertions on mocked Transactions DB and Notifier?

и тут возникают вопросы:
– откуда мы знаем, что submit вообще вызовет accounts.load?
– откуда мы знаем, что submit вызовет accounts.load с параметром sourceAccount.id?
– откуда мы знаем, что submit не вызовет других методов accounts?
Этого нет в публичном интерфейсе (здесь я имею ввиду публичные методы и конструкторы) класса Transfer, нет в его «контракте». Фактически это — деталь реализации.

Т.е. наличие параметра Accounts в публичном конструкторе — это часть контракта, публичная внешняя зависимость, а как именно используется экземпляр Accounts — это деталь реализации и «знание» этой детали в тесте — прямое нарушение инкапсуляции.

Теперь рассмотрим альтернативу:
    public final class Transfer {
    public static final class Request {
    public final Money amount;
    public final Lazy source;
    public final Lazy destination;
    public final Lazy checkCompliance;
    public Request(Money amount, Lazy source, Lazy destination, Lazy checkCompliance) {
    this.amount = amount;
    this.source = source;
    this.destination = destination;
    this.checkCompliance = checkCompliance;
    }
    }
    public static final class Response {
    public final Transaction transaction;
    public final Notification[] notifications;
    private Response(Transaction transaction, Notification... notifications) {
    this.transaction = transaction;
    this.notifications = notifications;
    }
    public static final class Notification {
    public final User.ID user;
    public final Transaction.ID transaction;
    public final String message;
    public Notification(User.ID user, Transaction.ID transaction, String message) {
    this.user = user;
    this.transaction = transaction;
    this.message = message;
    }
    }
    }
    public static final class InsufficientFunds extends RuntimeException {}
    public Response submit(Request r) {
    validate(r);
    return transaction(r);
    }
    private void validate(Request r) {
    checkCompliance(r);
    checkBalance(r);
    }
    private void checkCompliance(Request r) {
    if (r.source.val().hasSameOwnerAs(r.destination.val())) {
    return;
    }
    r.checkCompliance.val();
    }
    private void checkBalance(Request r) {
    if (r.source.val().canWithdraw(r.amount)) {
    return;
    }
    throw new InsufficientFunds();
    }
    private Response transaction(Request r) {
    final var transaction = Transaction.fresh(r.source.val().id, r.destination.val().id, r.amount);
    return new Response(
    transaction,
    new Response.Notification(r.source.val().owner, transaction.id, "withdraw"),
    new Response.Notification(r.destination.val().owner, transaction.id, "top up")
    );
    }
    }

Ни одной внешней зависимости с полным сохранением функциональности.

Примерный тест:
    final var sourceAccount = // new account
    ...
    final var response = subject.submit(new Transfer.Request(
    money,
    () -> sourceAccount,
    () -> destinationAccount,
    () -> { /* e.g. do nothing in this particular test case */ }
    ));
    // assertions on response

И никаких моков нафиг не надо.

https://forum.sources.ru/index.php?showtopic=419507&view=findpost&p=3838152

Метки:  

 

Добавить комментарий:
Текст комментария: смайлики

Проверка орфографии: (найти ошибки)

Прикрепить картинку:

 Переводить URL в ссылку
 Подписаться на комментарии
 Подписать картинку