Come dovrei fare per portare questo codice sotto test?

2

Sto lavorando su un framework di test open source .

Il 90% della mia base di codice ha una buona copertura di test. Il mio problema principale è il punto di ingresso della riga di comando . Questo modulo è nato come uno script molto breve per chiamare il modello di dominio (vedere la chiamata a _run_impl() ) e uscire con il codice 1 se l'esecuzione di test ha avuto esito negativo.

Non mi sembrava necessario testare quello script al momento in cui l'ho scritto, ma dal momento che il progetto ha funzionato, alcune piccole modifiche alla logica in questo file lo hanno portato a diventare piuttosto lungo:

import argparse
import os
import sys
from . import _run_impl  # a function, in __init__.py, which calls into the domain model
from . import reporting  # a sub-package concerned with writing output to the console

import colorama; colorama.init()

def cmd():
    parser = argparse.ArgumentParser()
    parser.add_argument('-s', '--no-capture',
        action='store_false',
        dest='capture',
        default=True,
        help="Disable capturing of stdout during tests.")
    parser.add_argument('-v', '--verbose',
        action='store_true',
        dest='verbose',
        default=False,
        help="Enable verbose progress reporting.")
    parser.add_argument('--teamcity',
        action='store_true',
        dest='teamcity',
        default=False,
        help="Enable teamcity test reporting.")
    parser.add_argument('--no-random',
        action='store_false',
        dest='shuffle',
        default=True,
        help="Disable test order randomisation.")
    parser.add_argument('path',
        action='store',
        nargs='?',
        default=os.getcwd(),
        help="Path to the test file or directory to run. (default current directory)")
    args = parser.parse_args()

    if args.teamcity or "TEAMCITY_VERSION" in os.environ:
        reporters = (reporting.teamcity.TeamCityReporter(sys.stdout),)
    elif args.verbose:
        reporters = (reporting.cli.ColouredReporter(sys.stdout),)
    elif args.capture:
        reporters = (
            reporting.cli.DotsReporter(sys.stdout),
            type("ColouredCapturingReporter", (reporting.cli.ColouredReporter, reporting.cli.StdOutCapturingReporter), {})(sys.stdout),
            reporting.cli.TimedReporter(sys.stdout)
        )
    else:
        reporters = (
            reporting.cli.DotsReporter(sys.stdout),
            type("ColouredCapturingReporter", (reporting.cli.ColouredReporter, reporting.cli.SummarisingReporter), {})(sys.stdout),
            reporting.cli.TimedReporter(sys.stdout)
        )

    reporter = reporting.shared.ReporterManager(*reporters)

    _run_impl(os.path.realpath(args.path), reporter, args.shuffle)

    if reporter.failed:
        sys.exit(1)
    sys.exit(0)


if __name__ == "__main__":
    cmd()

So che questo codice ha dei bug (per esempio, dovrebbe essere possibile catturare lo stdout dai test quando si esegue in modalità dettagliata), e ho perso la fiducia nella mia capacità di cambiare questo modulo senza rompere qualcosa. Quindi voglio scrivere alcuni test per questo - ma ho solo esperienza con lo sviluppo test-first, quindi non so da dove iniziare.

I test dovrebbero essere all'unità o al livello di integrazione? Devo prendere in giro il ArgumentParser ? Devo simulare il modulo reporting ? Devo simulare _run_impl ? Dovrei provare a portare l'intero modulo sotto test all'inizio, o semplicemente aggiungere test mentre aggiungo funzionalità? Devo refactoring per rendere questo metodo più testabile?

Addendum : Penso che una parte della mia confusione sia dovuta al fatto che questo è uno strato relativamente sottile in cima alla logica del business - è proprio lì per rendere possibile il calcio d'inizio test runner dalla riga di comando. Quindi mi sento confuso su ciò che costituisce il confine del servizio qui - in particolare, quello che dovrei e non dovrei prendere in giro.

    
posta Benjamin Hodgson 01.12.2013 - 22:58
fonte

3 risposte

4

Il test è solitamente difficile quando più idee sono accoppiate nello stesso codice; significa testare un sacco di cose allo stesso tempo. Rompere il codice a pezzi più piccoli risolve solitamente questo problema.

In questo senso, lo riscriveremo leggermente estrapolando alcuni metodi:

def cmd(arguments):
    args = parse(arguments)
    reporter = create_reporter(args)

    _run_impl(os.path.realpath(args.path), reporter, args.shuffle)

   return not reporter.failed

if __name__ == "__main__":
   if cmd(sys.argv)
       sys.exit(0)
   sys.exit(1)

Questo ha diversi vantaggi:

  • Il codice in cmd non è accoppiato a sys.argv e sys.exit (), entrambi difficili da gestire in un contesto di test dell'unità
  • cmd ora è semplice da testare se prendi in giro parse , create_reporter e _run_impl
  • parse , create_reporter sono semplici e abbastanza isolati da poter essere facilmente testati da soli.

Python non è la mia lingua principale, quindi non ricordo se sarà necessario creare alcune classi per abilitare la pulizia, ma se così fosse. È solo un po 'di battitura per un sacco di valore.

    
risposta data 03.12.2013 - 07:31
fonte
3

Penso che dovresti testare l'interfaccia della riga di comando come parte del test di integrazione.

Cambia la tua funzione principale in:

def cmd(args=None):
    ...
    args = parser.parse_args(args=args)
    ...

... in modo da poter passare facilmente un elenco di argomenti nei test.

Se eseguire l'intero programma durante il test non è ragionevole, prendi in giro _run_impl .

Potresti anche voler guardare qualcosa come scripttest per aiutare a catturare l'output.

    
risposta data 03.12.2013 - 20:37
fonte
2

Sembra che il lavoro principale sia tutto al di fuori di questo codice. quindi i test per questo codice devono solo dimostrare che le chiamate che eseguirà verranno eseguite in modo affidabile. Probabilmente potresti anche verificare che tutti gli elenchi dei "giornalisti" istanziano oggetti Reporter validi. Questo è tutto un caso perfettamente legittimo per semplici test di unità vecchie.

È certamente possibile suddividere il codice in bit più piccoli adatti per il test dell'unità: ad esempio, l'analisi dell'argomento potrebbe diventare una funzione che sarebbe abbastanza facile da verificare per diverse combinazioni di flag. Sembra che il compito principale di questo codice sia solo quello di compilare l'elenco dei giornalisti, quindi è necessario assicurarsi che gli argomenti producano gli elenchi previsti in modo deterministico. L'aumento della testabilità potrebbe anche suggerire un refactoring in cui "reporter" diventa una classe piuttosto che una lista in modo che tu possa capire i contenuti in modo strutturato - che sarà più verificabile e forse anche più gestibile lungo la strada, anche se è difficile dire senza conoscere le API

Nel complesso, tuttavia, sembra uno scenario abbastanza verificabile e, come gran parte del codice dell'interfaccia utente, uno che trarrebbe beneficio da una serie di test che assicurano che argomenti come quelli mal formati o inconsistenti siano gestiti in modo affidabile.

    
risposta data 02.12.2013 - 00:49
fonte

Leggi altre domande sui tag