In che modo TDD indirizza l'interazione tra gli oggetti?

4

I sostenitori del TDD sostengono che ciò si traduce in un design migliore e in oggetti disaccoppiati. Posso capire che i test di scrittura impongono innanzitutto l'uso di cose come l'iniezione delle dipendenze, che si traduce in oggetti liberamente accoppiati. Tuttavia, TDD si basa su test unitari, che testano i singoli metodi e non l'integrazione tra gli oggetti. Eppure, TDD si aspetta che il design si evolva dai test stessi.

Quindi, come può TDD ottenere un design migliore a livello di integrazione (cioè inter-oggetto) quando la granularità che indirizza è più fine di quella (metodi individuali)?

    
posta Gigi 02.10.2013 - 19:25
fonte

7 risposte

6

Non sono d'accordo con il presupposto che i test unitari debbano essere di tipo metodo. I test unitari non devono essere limitati a testare i singoli metodi, ma possono anche verificare le interazioni tra gli oggetti.

La differenza tra un test unitario e un test di integrazione è che il test di integrazione richiede la presenza di qualche tipo di risorsa esterna (database, coda, file system, ecc.). Il test unitario userebbe i mock per sostituire le cose ai limiti dell'applicazione.

C'è un'idea che i test unitari dovrebbero essere piccoli. L'articolo di Wikipedia su Unit test dice:

In computer programming, unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures are tested to determine if they are fit for use.1 Intuitively, one can view a unit as the smallest testable part of an application. In procedural programming, a unit could be an entire module, but is more commonly an individual function or procedure. In object-oriented programming, a unit is often an entire interface, such as a class, but could be an individual method. [2] Unit tests are created by programmers or occasionally by white box testers during the development process.

Quindi l'idea di cosa costituisce un'unità sembra molto flessibile per me.

Non c'è motivo per cui un test unitario non possa istanziare oggetti diversi e testare i risultati del loro utilizzo insieme, questo può essere un modo prezioso per testare che il codice sia internamente coerente.

    
risposta data 02.10.2013 - 19:49
fonte
3

Dipende. In TDD non è necessario che esista una relazione uno-a-uno tra un test e una classe. Il refactoring può produrre molte classi che sono essenzialmente sotto una serie di test. In questo senso, le interazioni di classe vengono testate.

Detto questo, ci sono dei confini in cui prendi in giro determinate interazioni, che si tratti di un sistema di derisione o solo in termini di disaccoppiamento. Questi limiti sono soggetti a test diversi, che vanno oltre il TDD. In termini di TDD, si rende la superficie e la complessità della creazione dell'accoppiamento il più piccolo possibile e quindi l'accoppiamento effettivo non viene testato da TDD.

L'accoppiamento dovrebbe generalmente essere una linea o due di codice. Se è troppo complicato, potrebbe essere necessario testare il meccanismo di accoppiamento indipendentemente dall'intero sistema, ma questo torna a uno di quei limiti.

Quindi ti rimane un problema che va oltre il TDD - Ho scritto una classe o un gruppo di classi per fare A, con l'idea che la classe o il gruppo di classi B interagisse con esso. Ma ho rovinato tutto? C'è una sottile differenza in quello che stavo pensando quando ho scritto uno vs cosa stavo pensando quando ho scritto l'altro? E, naturalmente, in un progetto multi-persona, ciascuna parte potrebbe non essere stata scritta dalla stessa persona.

Questo è test di integrazione, test di accettazione, controllo qualità, ecc. È un test dopo, non guidato da test, anche se i test di accettazione sono stati scritti prima, non è proprio lo stesso.

    
risposta data 02.10.2013 - 19:42
fonte
0

Penso che tu voglia guardare ai test di integrazione, dove potresti prendere in giro le classi che sono i singoli componenti e i loro risultati attesi ed eseguire azioni su una collezione di quelli.

Dovresti impostare l'output di test o oggetti mocked su valori noti / previsti, quindi stai semplicemente testando i metodi che interagiscono su quegli specifici oggetti.

Fai un esempio in cui hai due classi, Person e Car per esempio. I singoli test di unità basati su classi testeranno le funzioni su ciascuna di queste classi Person.SetName() e Car.SetMake() per esempio.

Il tuo test di integrazione potrebbe essere il test di un CarParkService in cui dovresti passare in una macchina e una persona in uno stato conosciuto (potenzialmente deriso) e chiamare il metodo ParkACar(Person p, Car c) . Con questo modo di lavorare stai semplicemente testando l'integrazione della persona e dell'auto nel contesto di un servizio.

Puoi testarlo molto più a fondo, ma per quanto lo vedo, in un certo senso test di integrazione testeranno la combinazione di classi / metodi come un gruppo (come un servizio).

    
risposta data 02.10.2013 - 19:32
fonte
0

Sfortunatamente la definizione di unit test è molto sfocata. Una delle definizioni più citate è fatta attraverso le proprietà che dovrebbero avere i test, che sono

  • corre veloce
  • è isolato (nessuna interazione tra i test, ad esempio eseguirli in qualsiasi ordine e ottenere sempre lo stesso risultato)
  • non richiede alcuna configurazione esterna
  • fornisce un risultato pass / fail coerente È tratto dal libro di Roy Osherove, l'arte del collaudo di unità.

Se dovessi aggiungere una definizione, direi che un test unitario si basa sul risultato di un percorso logico attraverso un metodo in cui hai il controllo sul risultato di tutte le dipendenze coinvolte (fw, questa è solo la mia definizione). / p>

Immagina di dover creare un metodo che controlli se una stringa è valida, e in tal caso fa qualcosa, altrimenti fa qualcosa di diverso. Ecco come potremmo scrivere quel metodo se vogliamo che sia unit testabile:

class SomeClass
{
    IStringAnalyzer stringAnalizer;
    ILogger logger;

    public SomeClass(IStringAnalyzer stringAnalyzer, ILogger logger)
    {    
        this.logger = logger;
        this.stringAnalyzer = stringAnalyzer;
    }


public void SomeMethod(string someParameter)
    {

    if (stringAnalyzer.IsValid(someParameter))
    {
        //do something with someParameter
    }else
    {
        logger.Log("Invalid string");
    }
}

stringAnalyser e logger sono delle dipendenze perché vogliamo essere in grado di controllare ciò che restituiscono (attraverso uno stub) e come vengono utilizzati (tramite una simulazione).

Questo è così puoi scrivere un test come questo (che ha tutte le belle caratteristiche che descrive Roy nel suo libro):

[Test]
public void SomeMethod_InvalidParameter_CallsLogger
{
    Rhino.Mocks.MockRepository mockRepository = new Rhino.Mocks.MockRepository();
    IStringAnalyzer s = mockRepository.Stub<IStringRepository>();
    s.Stub(s => s.IsValid("something, doesnt matter").IgnoreParameters().Return(false);
    ILogger l = mockRepository.DynamicMock<ILogger>();
    SomeClass someClass = new SomeClass(s, l);
    mockRepository.ReplayAll();

    someClass.SomeMethod("What you put here doesnt really matter because the stub will always return false");

    l.AssertWasCalled(l => l.Log("Invalid string"));
}

I vantaggi sono che le "molte preoccupazioni" sono tutte disaccoppiate e sono molto facili da individuare quando si esegue il test per primo. Vedresti che dovresti lasciare la logica di StringAnalizer fuori dalla classe che stai testando perché la necessità di controllare se una stringa è valida o meno sarebbe evidente.

Quel test fa uso di un framework di isolamento (rhino mocks) e spiego cosa è qui: link

Per quanto riguarda la discussione di TDD produce davvero disegni migliori, tutto quello che posso dire è questo: link E questo: link

    
risposta data 04.10.2013 - 00:47
fonte
0

So how can TDD possibly result in a better design at the integration (i.e. inter-object) level when the granularity it addresses is finer than that (individual methods) ?

Perché granularità si evolve come conseguenza di TDD :)

Il fatto che il punto di ingresso in un test (parte Act in Arrange Act Assert) sia un singolo metodo non significa che tutto il codice di produzione risultante sarà contenuto in quel metodo alla fine del ciclo TDD.

Un passo (il più?) importante in TDD è refactoring . Attraverso il refactoring, il codice può essere spostato su un altro metodo o un'altra classe.

    
risposta data 04.10.2013 - 14:55
fonte
0

Per rispondere alla tua domanda, direi, ciò che affermi è giusto, in parte.

TDD fa ampio uso di test unitari, ma anche test di accettazione. Se osservi l'intero ciclo di test TDD descritto in Software orientato agli oggetti in crescita, guidato dai test , è:

  1. Scrivi un test di accettazione non soddisfacente
  2. Scrivi un test dell'unità in errore
  3. Esegui il test falling
  4. Refactor
  5. Ripeti i passaggi 2-4 fino a quando il test di accettazione non riesce supera

Per definizione i test di accettazione sono test end-to-end che partono dal livello di input dell'utente attraverso tutto lo stack di applicazioni (inclusi oggetti di dominio, servizi esterni e database).

Per questo motivo non testerai solo i tuoi componenti e definirai l'interazione, ma li baserai anche su test di accettazione globali che dimostrano che i tuoi componenti lavorano insieme.

Questo ti fornirà una certezza sufficiente in modo da poter imporre miglioramenti strutturali e architettonici al tuo codice quando richiesto.

Se vuoi vedere questo in azione, controlla questa sessione TDD con Zio Bob.

    
risposta data 03.10.2013 - 16:55
fonte
0

So che questo post è piuttosto vecchio, ma ho pensato che sarebbe stato utile dare un esempio pratico di come i test UNIT possano effettivamente supportare la progettazione del SISTEMA.

Ho incollato sotto un codice che penso risponda a questa domanda dall'OP:

TDD is based on unit tests.. and not the integration between objects...So how can it possibly result in a better design at the integration (i.e. inter-object) level.

Una risposta a questo è definire e testare l'API dell'intero sistema, ad esempio, in una raccolta di mock per tutte le unità, basata su interfacce (non implementazioni) PRIMA di fare alcuna implementazione. Quindi scrivi TUTTI i test unitari relativi a questa raccolta e, per definizione, tutti i test dovrebbero passare.

Continua a modificare il design generale del sistema fino a renderlo felice, quindi implementa le interfacce una alla volta, in ogni caso disattiva l'interfaccia simulata per la nuova implementazione della classe e il refactoring fino a quando non passa.

Nell'esempio seguente ci sono solo due interfacce minime chiamate IUnit1 e IUnit2 e una classe che è responsabile della definizione dell'API tra di loro, chiamata MockApi. Esiste quindi una classe di test con test unitari per le due unità, ciascuna delle quali può essere eseguita con l'interfaccia fittizia o l'implementazione, se disponibile. Infine ci sono implementazioni chiamate Unità1 e Unità2.

Questo esempio usa Moq e XUnit, ma ovviamente potrebbe applicarsi a qualsiasi piattaforma.

using Moq;
using Xunit;

namespace TDD_with_API
{
    public interface IUnit1
    {
        string GetMessage(IUnit2 unit2);
    }
    public interface IUnit2
    {
        string GetExclamation();
        string Name { get; set; }
    }
    public class MockApi
    {
        public MockApi()
        {
            // SET UP THE MOCKS AND EXPOSE THEIR OBJECTS AS PROPERTIES:
            var mock_unit1 = new Mock<IUnit1>();
            Unit1 = mock_unit1.Object;
            var mock_unit2 = new Mock<IUnit2>();
            Unit2 = mock_unit2.Object;
            //    ..etc for all the units in the API

            // DEFINE THE API BY EXAMPLE:
            mock_unit2.Setup(u => u.GetExclamation()).Returns("Hello");
            mock_unit2.Setup(u => u.Name).Returns("World");
            mock_unit1.Setup(u => u.GetMessage(It.IsAny<IUnit2>()))
                .Returns((IUnit2 u) => u.GetExclamation() + " " + u.Name + "!");
        }
        public IUnit1 Unit1 { get; set; }
        public IUnit2 Unit2 { get; set; }
    }
    public class UnitTests
    {
        private MockApi _api;
        public UnitTests()
        {
            _api = new MockApi();
        }
        [Theory]
        [InlineData(true)] // Comment this line out until class implemented
        [InlineData(false)]
        public void Test_Unit1(bool testThisUnitForReal)
        {
            IUnit1 unitToTest = testThisUnitForReal ?
                new Unit1() : _api.Unit1;
            Assert.Equal(unitToTest.GetMessage(_api.Unit2), "Hello World!");
        }
        [Theory]
        [InlineData(true)] // Comment this line out until class implemented
        [InlineData(false)]
        public void Test_Unit2(bool testThisUnitForReal)
        {
            IUnit2 unitToTest = testThisUnitForReal ?
                new Unit2() : _api.Unit2;
            Assert.Equal(unitToTest.GetExclamation(), "Hello");
            Assert.Equal(unitToTest.Name, "World");
        }
    }
    public class Unit1 : IUnit1
    {
        public string GetMessage(IUnit2 unit2)
        {
            return string.Format("{0} {1}!",
                unit2.GetExclamation(),unit2.Name);
        }
    }
    public class Unit2 : IUnit2
    {
        public string Name { get; set; } = "World";
        public string GetExclamation()
        {
            return "Hello";
        }
    }
}
    
risposta data 02.02.2017 - 22:58
fonte

Leggi altre domande sui tag