File singoli o multipli per test unitari di una singola classe?


20

Nel ricercare le migliori pratiche di unit test per aiutare a mettere insieme le linee guida per la mia organizzazione, ho incontrato la questione se sia meglio o utile separare i dispositivi di test (classi di test) o mantenere tutti i test per una singola classe in un file.

A proposito, mi riferisco ai "test unitari" nel senso puro che si tratta di test in white box rivolti a una singola classe, un'asserzione per test, tutte le dipendenze derise, ecc.

Uno scenario di esempio è una classe (chiamalo Documento) che ha due metodi: CheckIn e CheckOut. Ogni metodo implementa varie regole, ecc. Che controllano il loro comportamento. Seguendo la regola dell'asserzione per test, avrò più test per ciascun metodo. Posso mettere tutti i test in una singola DocumentTestsclasse con nomi come CheckInShouldThrowExceptionWhenUserIsUnauthorizede CheckOutShouldThrowExceptionWhenUserIsUnauthorized.

Oppure, potrei avere due classi di test separate: CheckInShoulde CheckOutShould. In questo caso, i miei nomi dei test verrebbero abbreviati ma sarebbero organizzati in modo che tutti i test per un comportamento specifico (metodo) siano uniti.

Sono sicuro che ci sono pro e contro di entrambi gli approcci e mi chiedo se qualcuno ha percorso il percorso con più file e, in caso affermativo, perché? Oppure, se hai optato per l'approccio a file singolo, perché ritieni che sia migliore?


2
I nomi dei metodi di prova sono troppo lunghi. Semplificali, anche se ciò significa che un metodo di test conterrebbe più asserzioni.
Bernard,

12
@Bernard: è considerato una cattiva pratica avere più asserzioni per metodo, i nomi lunghi dei metodi di prova NON sono considerati cattive pratiche. Di solito documentiamo ciò che il metodo sta testando nel nome stesso. Ad esempio constructor_nullSomeList (), setUserName_UserNameIsNotValid () ecc ...
c_maker,

4
@Bernard: uso anche nomi di test lunghi (e solo lì!). Li adoro, perché è così chiaro cosa non funzionava (se i tuoi nomi sono buone scelte;)).
Sebastian Bauer,

Affermazioni multiple non sono una cattiva pratica, se tutte testano lo stesso risultato. Per esempio. non avrestitestResponseContainsSuccessTrue() , testResponseContainsMyData()e testResponseStatusCodeIsOk(). Li avrebbe in un singolo testResponse()che ha tre afferma: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)eassertEquals(true, response.success)
Juha Untinen

Risposte:


18

È raro, ma a volte ha senso avere più classi di test per una determinata classe sotto test. In genere lo farei quando sono necessarie impostazioni diverse e condivise in un sottoinsieme dei test.


3
Un buon esempio del perché questo approccio mi è stato presentato in primo luogo. Ad esempio, quando voglio testare il metodo CheckIn, voglio sempre che l'oggetto sotto test sia impostato in un modo, ma quando collaudo il metodo CheckOut, ho bisogno di una configurazione diversa. Buon punto.
SonOfPirate,

14

Non riesco davvero a vedere alcun motivo convincente per cui dovresti dividere un test per una singola classe in più classi di test. Poiché l'idea di guida dovrebbe essere quella di mantenere la coesione a livello di classe, dovresti impegnarti anche a livello di test. Solo alcuni motivi casuali:

  1. Non è necessario duplicare (e mantenere più versioni di) codice di configurazione
  2. È più semplice eseguire tutti i test per una classe da un IDE se sono raggruppati in una classe di test
  3. Risoluzione dei problemi più semplice con mapping one-to-one tra classi di test e classi.

Punti buoni. Se la classe sotto test è un kludge terribile, non escluderei diverse classi di test, in cui alcune contengono una logica di supporto. Non mi piacciono le regole molto rigide. A parte questo, ottimi punti.
Giobbe

1
Eliminerei il codice di installazione duplicato disponendo di una classe base comune per le apparecchiature di test che funzionano con una singola classe. Non sono affatto d'accordo con gli altri due punti.
SonOfPirate,

In JUnit, ad esempio, potresti voler testare le cose usando diversi Runner, il che porta alla necessità di classi diverse.
Joachim Nilsson,

Mentre scrivo una classe con un gran numero di metodi con matematica complessa in ciascuno di essi, trovo di avere 85 test e finora ho scritto solo test per il 24% dei metodi. Mi trovo qui perché mi chiedevo se è folle in qualche modo dividere questo enorme numero di test in categorie separate in modo da poter eseguire sottoinsiemi.
Mooing Duck,

7

Se sei costretto a dividere i test unitari per una classe su più file, ciò potrebbe indicare che la classe stessa è progettata male. Non riesco a pensare ad alcuno scenario in cui sarebbe più vantaggioso suddividere i test unitari per una classe che aderisce ragionevolmente al principio di responsabilità singola e ad altre migliori pratiche di programmazione.

Inoltre, è accettabile avere nomi di metodo più lunghi nei test unitari, ma se ti dà fastidio, puoi sempre ripensare la convenzione di denominazione dei test unitari per abbreviare i nomi.


Purtroppo, nel mondo reale non tutte le classi aderiscono a SRP et al ... e questo diventa un problema quando si esegue il test del codice legacy dell'unità.
Péter Török,

3
Non sono sicuro di come SRP si collega qui. Ho più metodi nella mia classe e ho bisogno di scrivere unit test per tutti i diversi requisiti che guidano il loro comportamento. È meglio avere una classe di test con 50 metodi di test o 5 classi di test con 10 test ciascuno relativi a un metodo specifico nella classe in prova?
SonOfPirate,

2
@SonOfPirate: se hai bisogno di così tanti test, ciò potrebbe significare che i tuoi metodi stanno facendo troppo. Quindi SRP è molto rilevante qui. Se applichi SRP alle classi e ai metodi, avrai molte piccole classi e metodi che sono facili da testare e non avrai bisogno di così tanti metodi di prova. (Ma sono d'accordo con Péter Török che quando testate il codice legacy, le buone pratiche sono fuori dalla porta e dovete solo fare il meglio che potete con ciò che vi viene dato ...)
c_maker,

Il miglior esempio che posso dare è il metodo CheckIn in cui abbiamo regole di business che determinano chi è autorizzato a eseguire l'azione (autorizzazione), se l'oggetto è nello stato corretto per essere archiviato, ad esempio se è stato estratto e se cambia sono stati fatti. Avremo test unitari per verificare l'applicazione delle regole di autorizzazione e test per verificare che il metodo si comporti correttamente se viene chiamato CheckIn quando l'oggetto non è stato estratto, non modificato, ecc. Ecco perché finiamo con molti test. Stai suggerendo che questo è sbagliato?
SonOfPirate,

Non credo che ci siano risposte concrete, giuste o sbagliate su questo argomento. Se quello che stai facendo funziona per te, è grandioso. Questa è solo la mia prospettiva su una linea guida generale.
FishBasketGordo

2

Uno dei miei argomenti contro la separazione dei test in più classi è che diventa più difficile per gli altri sviluppatori del team (in particolare quelli che non sono esperti di test) individuare i test esistenti ( Accidenti, mi chiedo se esiste già un test per questo metodo? Mi chiedo dove sarebbe? ) e anche dove mettere nuovi test ( ho intenzione di scrivere un test per questo metodo in questa classe, ma non sono sicuro se dovrei inserirli in un nuovo file o in un esistente uno? )

Nel caso in cui test diversi richiedano impostazioni drasticamente diverse, ho visto alcuni professionisti di TDD inserire "fixture" o "setup" in classi / file diversi, ma non i test stessi.


1

Vorrei poter ricordare / trovare il link in cui è stata dimostrata per la prima volta la tecnica che ho scelto di adottare. In sostanza, creo una singola classe astratta per ogni classe sotto test che contiene dispositivi di test nidificati (classi) per ciascun membro sottoposto a test. Ciò fornisce la separazione originariamente desiderata, ma mantiene tutti i test nello stesso file per ogni posizione. Inoltre, questo genera un nome di classe che consente un facile raggruppamento e ordinamento nel test runner.

Ecco un esempio di come questo approccio viene applicato al mio scenario originale:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

Ciò comporta la visualizzazione dei seguenti test nell'elenco dei test:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

Man mano che vengono aggiunti altri test, questi vengono facilmente raggruppati e ordinati in base al nome della classe, il che mantiene anche tutti i test per la classe in prova elencati insieme.

Un altro vantaggio di questo approccio che ho imparato a sfruttare è la natura orientata agli oggetti di questa struttura significa che posso definire il codice all'interno dei metodi di avvio e pulizia della classe base DocumentTests che sarà condiviso da tutte le classi nidificate e all'interno di ciascuna classe nidificata per i test che contiene.


1

Prova a pensare al contrario per un minuto.

Perché dovresti avere solo una classe di test per classe? Stai facendo un test di classe o un test unitario? Dipendi anche dalle lezioni ?

Un test unitario dovrebbe testare un comportamento specifico, in un contesto specifico. Il fatto che la tua classe abbia già un CheckInmezzo dovrebbe esserci un comportamento che ne ha bisogno in primo luogo.

Come penseresti di questo pseudo-codice:

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Ora non stai testando direttamente il checkIn metodo. Stai invece testando un comportamento (che è quello di fare il check-in per fortuna;)), e se hai bisogno di refactoring Documente dividerlo in diverse classi, o unire un'altra classe in esso, i tuoi file di test sono ancora coerenti, hanno ancora senso dal non stai mai cambiando la logica durante il refactoring, solo la struttura.

Un file di test per una classe rende semplicemente più difficile il refactoring ogni volta che ne hai bisogno e anche i test hanno meno senso in termini di dominio / logica del codice stesso.


0

In generale, consiglierei di pensare ai test come a testare un comportamento della classe piuttosto che un metodo. Vale a dire che alcuni dei tuoi test potrebbero dover chiamare entrambi i metodi sulla classe per testare alcuni comportamenti previsti.

Di solito inizio con una classe di test unitari per classe di produzione, ma alla fine posso suddividere quella classe di test unitari in diverse classi di test in base al comportamento che testano. In altre parole, consiglierei di non dividere la classe di test in CheckInShould e CheckOutShould, ma piuttosto di suddividere in base al comportamento dell'unità in prova.


0

1. Considera cosa potrebbe andare storto

Durante lo sviluppo iniziale, sai abbastanza bene cosa stai facendo ed entrambe le soluzioni probabilmente funzioneranno bene.

Diventa più interessante quando un test fallisce dopo una modifica molto più tardi. Vi sono due possibilità:

  1. Hai seriamente rotto CheckIn(o CheckOut). Anche in questo caso, sia la soluzione a file singolo che quella a due file sono OK.
  2. Hai modificato entrambi CheckIne CheckOut(e forse i loro test) in modo sensato per ciascuno di essi, ma non per entrambi. Hai rotto la coerenza della coppia. In tal caso, suddividere i test su due file renderà più difficile la comprensione del problema.

2. Considera a cosa servono i test

I test hanno due scopi principali:

  1. Controlla automaticamente che il programma funzioni ancora correttamente. Se i test sono in uno o più file non è importante per questo scopo.
  2. Aiuta un lettore umano a dare un senso al programma. Ciò sarà molto più semplice se i test per funzionalità strettamente correlate saranno tenuti insieme, poiché vedere la loro coerenza è una strada rapida per la comprensione.

Così?

Entrambe queste prospettive suggeriscono che tenere insieme i test può aiutare, ma non farà male.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.