DRY TDD + test delle unità

0

Dove / come si disegna la linea per le classi di test unitari a diversi livelli di una gerarchia? Ad esempio, supponiamo di avere una struttura come questa:

public class Account
{
    public Money CurrentBalance { get; } => ComputeBalance(_transactions);

    public Money BalanceByDate(DateTime date)
    {
        // ComputeBalance through date
    }
    // More accounting functions
}

public class Ledger
{
    public void CreateNewAccount(/* Account Details */)
    {
        // Create a new account
        _accounts.Add(account);
    }

    public Account GetAccountByName(AccountName name)
    {
        // return an account if/when found by name
    }

    public void PostTransaction(Transaction transaction)
    {
        // add a transaction to two different accounts
    }

    // More ledger functions

    private readonly List<Account> _accounts = new List<Account>();
}

Ho una classe AccountTests che ovviamente verifica le funzioni dell'account come CurrentBalance, BalanceByDate (), ecc. Tuttavia, la domanda è:

D) Quanto di Ledger devo testare? Devo solo controllare che Ledger restituisca un account? Oppure devo ricontrollare tutte le funzioni dell'account poiché Ledger pubblicherà le transazioni su 2 account diversi (ad esempio, controlla nuovamente CurrentBalance ma questa volta per ogni coppia di account (account accreditato e conto addebitato).

Sto facendo TDD (anche se non sono sicuro di quanto bene), e sento che sto testando più cose diverse volte.

    
posta keelerjr12 13.11.2018 - 00:55
fonte

2 risposte

2

I test sono un po 'di un'arte nera. Pensala come una spiegazione su come utilizzare il tuo sistema e qualsiasi trucco che vorresti dire a un altro sviluppatore.

Quindi nel tuo caso con il libro mastro. Dovresti fare un test per ogni scenario di utilizzo valido. Trattando l'oggetto come una scatola nera, qualsiasi funzione / tipo / metodo / tipo disponibile esternamente dovrebbe essere parte di almeno un utilizzo valido dell'oggetto in produzione.

Se c'è qualcosa esposto che non fa parte di un uso valido dell'oggetto. Questo è un altro sviluppatore che non dovrebbe mai chiamare ragionevolmente tale funzione, quindi si trova nel posto sbagliato o è candidato alla cancellazione.

  • Se la funzione è nel posto sbagliato, spostala. Questo è quello che i Test ti stanno dicendo di fare.

  • Se la funzione non viene utilizzata, eliminala. Codice Morto, è un codice che può darti una brutta giornata. Non tenerlo in giro per ogni evenienza. Il repository del codice sorgente ti permette di guardarlo e rilanciarlo se ne hai bisogno in seguito.

Detto questo, il test dovrebbe mostrare qualcosa nuovo . Con nuovo intendo che non avrei potuto apprenderlo sull'oggetto attraverso nessuno degli altri test. In sostanza non ripeterti.

Kevlin Henney ha diverse presentazioni su youtube su errori e test. Uno si chiama GUTS (Good Unit Test). Vale bene il tempo di ascoltare.

Quindi in pseudo-codice (perché il problema non è specifico per la lingua)

Test("Find named account in ledger returns the account")
{
    var exisitingAccount = Account(<setup with name "abc">);
    var ledger = Ledger(exisitingAccount, <and others>);

    var account = ledger.GetAccountByName("abc");

    //if the account is returned by reference
    Expect(account).to.be(exisitingAccount);

    //if the account is returned by value
    Expect(account).to.equal(exisitingAccount);
}

Valido copre anche gli scenari di "errore" previsti

Test("Find unknown named account in ledger [returns null|nullAccount]|[throws SomeException]")
{
    var ledger = Ledger();
    ledger.CreateNewAccount(<an account>);
    ledger.CreateNewAccount(<another account>);
    ledger.CreateNewAccount(<because this use case is legite>);
    ledger.CreateNewAccount(<accessing the accounts after they have been created>);

    //if the absence does not cause a thrown exception
    var account = ledger.GetAccountByName("abc");

    //if null means no account
    Expect(account).to.be(null);

    //if a special "null" account
    Expect(account).to.be(nullAccount);

    //or if it throws
    var exception = ExpectThrows(() => ledger.GetAccountByName("abc"));

    //always make sure the exception is correct, it is after-all a return value.
    Expect(exception.message).to.equal("important message");
}
    
risposta data 13.11.2018 - 07:16
fonte
2

Where/how do you draw the line for unit-testing classes at different levels of a hierarchy?

Jim Coplien ha fatto delle alcune osservazioni interessanti sull'utilizzo dei test pochi anni fa.

But what fraction of that interface is actually used by the application? And how much of the generalized engineering is really needed by the app?

Una struttura di dati / comportamento completamente generalizzata in genere supporta molti più casi d'uso di quelli effettivamente necessari nel contesto di un sistema specifico. Questa è la regola di scomposizione di Weinberg in azione - la parte è più di una frazione dell'intero - esponendo dettagli del sistema che altrimenti sarebbero nascosti aumenta l'area di superficie testabile, ma non necessariamente aumenta la valore dei test.

Contro questo, hai argomenti come quello di J.B. Rainsberger: I test integrati sono una truffa .

In definitiva, penso che questi due discorsi di Gary Bernhardt siano più influenzati dal pensiero:

La mia posizione attuale: una volta che mi trovo a un punto in cui sto lavorando con una funzione , smetto di preoccuparmi dei livelli di gerarchia sottostanti.

How much of Ledger do I need to test? Do I just check that Ledger returns an Account?

Kent Beck, nel 2008 :

I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence

    
risposta data 13.11.2018 - 05:05
fonte

Leggi altre domande sui tag