Come rendere più testabile la complessa logica di business con molte dipendenze?

1

Questa è una cosa che mi è venuta in mente per la maggior parte del tempo, ma non ho ancora trovato un buon approccio. Quindi ecco la cosa. Abbiamo un'applicazione server che ha un numero limitato di casi d'uso complessi che coinvolgono un sacco di problemi e gestione dei dati dei clienti. Non posso entrare nei dettagli esatti qui, quindi facciamo un esempio per alcuni contesti. Supponiamo che tu abbia un sistema di elaborazione degli ordini in cui elabori gli ordini per un cliente. Usiamo Java / Spring con l'iniezione delle dipendenze e tutti i campanelli e fischietti, quindi il nostro servizio sarebbe simile a questo:

class OrderService {

    // these are all dependency-injected by Spring -leaving this out for clarity.
    private CustomerService customerService;
    private OrderDao orderDao;
    private DiscountCodeService discountCodeService;
    private CustomerLoyaltyProgramService loyaltyProgramService;
    private TaxService taxService;


    public Order createOrder(OrderForm form) {
         // form is pre-validated through JSR303 annotations but there are 
         // some additional validations
         // that need to be made
         // 
         if (formHasError) {
              throw SomeException();
         }

         // ok we can continue here.
         Customer customer = customerService.getCustomerById(form.getCustomerId())

         // build up an order from the form
         Order order = new Order();
         // copy over stuff..

         // check if customer has a discount code
         if (form.getDiscountCode() != null) {
             discountCodeInfo = discountCodeService.getDiscountCodeInfoFor(form.getDiscountCode())
             // now walk over line items to see if there is some applicable order line item.
         }

         // check if the customer is in any loyalty program that will give him 
         // additional discounts
         List<LoyaltyProgram> programs = loyaltyProgramService.findByCustomer(customer);
         // do some more ifs and elses to apply loyalty program discounts

         // check applicable sales tax for customer
         SalesTax tax = taxService.findSalesTaxFor(form.getAddress())

         // do some tax calculations, more ifs and elses

         // finally store order
         orderDao.save(order);
         return order;

    }

}

Testare un tale servizio con un test unitario è fondamentalmente un incubo. Devi prendere in giro tutte le dipendenze in cui la derisione dipende dal flusso attraverso il metodo e dalle diverse regole aziendali applicate su determinate circostanze. A una certa quantità di mock e code path la tua testa esploderà semplicemente.

Un altro approccio che abbiamo provato è stato dividerlo in diversi metodi privati (ben protetti, in quanto è necessario accedervi in un test), che lo renderebbero più testabile. Tuttavia, la nostra logica aziendale è diffusa ovunque e questo rende difficile seguire il codice e devi ancora avere un "metodo master" che richiami tutti i piccoli metodi di supporto nell'ordine corretto ed è necessario testarlo anche per te in pratica torniamo al punto di partenza.

Quindi quello che sto cercando è un modo per riorganizzare ciò che consente di testare i metodi più facilmente senza dover pensare a tutte le dipendenze pur mantenendo il codice manutenibile e non diffondendolo in mille luoghi diversi dove non puoi più seguire la logica. Immagino che questo sarà probabilmente un compromesso, ma mi chiedo come potresti averlo affrontato nei tuoi progetti.

    
posta kork 10.08.2017 - 09:47
fonte

2 risposte

5

Penso che questo metodo stia facendo troppo. Non è coeso. Divisione in metodi privati non risolve il problema qui, e sarà probabilmente peggiorare ulteriormente .

Penso che dovresti cambiare la progettazione in modo che diversi componenti possano collaborare. Ogni componente sarà piccolo, coeso e focalizzato.

Per il tuo specifico esempio, consiglierei una catena di filtri . Una catena di filtri è un elenco collegato di processori che vengono eseguiti in sequenza e ciascun collegamento nella catena può scegliere di eseguire l'esecuzione su cauzione o regolare il messaggio che viene passato attraverso la catena.

Puoi ridisegnare il tuo metodo in diversi link in questo modo:

Ogni componente avrà solo un paio di dipendenze, e puoi creare la catena nella tua radice di composizione.

OrderProcessor persistOrder = new PersistOrderProcessor(null);
OrderProcessor taxOrder = new TaxOrderProcessor(taxService, persistOrder);
OrderProcessor discountCodeProcessor = new DiscountCodeOrderProcessor(discountCodeService, taxOrder);
OrderProcessor loyaltyProcessor = new LoyaltyOrderProcessor(loyaltyService, discountCodeProcessor);
OrderProcessor root = new ValidateOrderProcessor(loyaltyProcessor);

L'implementazione di ogni processore è simile a questa:

public Order process(order: Order) throws OrderProcessorException {
    // optionally validate order
    if (!valid) {
        throw new InvalidOrderException(...);
    }
    // modify order
    order.setTax(.3f * order.getSubtotal());
    // continue the chain (or decide not to)
    if (next != null) {
        order = next.process(order);
    }
    // optionally modify the order again
    return order;
}

Quindi modifica OrderService per iniettare OrderProcessor come dipendenza e avviare la catena.

class OrderService {

    // these are all dependency-injected by Spring -leaving this out for clarity.
    private OrderProcessor orderProcessor;

    public Order createOrder(OrderForm form) { 
         // build up an order from the form
         Order order = new Order();
         // copy over stuff..

         // start the chain
         return orderProcessor.process(order);
    }
} 

Questo design consente di testare separatamente ogni parte della pipeline degli ordini con uno o due mock. Hai un design scalabile e flessibile per le tue crescenti esigenze per aggiungere più logica al posizionamento degli ordini. Ogni fase della catena di elaborazione è piccola e coesa. La denominazione del componente è indicativa della responsabilità e sarà facile per gli altri utenti di navigare.

    
risposta data 10.08.2017 - 12:36
fonte
0

Forse, testare il front-end potrebbe servire meglio a quel punto rispetto al test unitario. Pensa a un server di prova che si inizializza in uno stato definito (record di test noti nel database), quindi simula tutti i processi di ordinazione utilizzando un framework come Selenium , e in seguito verifica se tutti gli ordini sono stati scritti correttamente nel database.

    
risposta data 10.08.2017 - 10:09
fonte

Leggi altre domande sui tag