Rendi testabile UnitOfWork

1

Sono in fase di refactoring del codice legacy. Sfortunatamente si tratta di un'applicazione WebForms con un codice molto stretto e maleodorante. Attualmente l'accesso al database è all'interno di modelli che assomigliano a questo:

var fooModel = new FooModel();
fooModel.Select(sqlConnection); // Accesses db and populates model's data.

Vorrei spostare questa logica in Data Access Layer e nel processo renderlo facile da testare, quindi in futuro potrò migrare a MVC senza troppi problemi. Attualmente il codice non usa alcun ORM, quindi devo attenermi a SqlConnection .

Ho trovato un modo per astrarre l'accesso al database, ma non so come renderlo facile da testare. Ho deciso di creare questa gerarchia:

Controller(currently codebehind) -> Services -> UnitOfWork -> Repositories -> SqlConnection to db .

Fondamentalmente Controllers può vedere solo Services che utilizza UnitOfWork per accedere a Repositories per utilizzare le transazioni e recuperare i dati.

Ho creato un campione per mostrarti cosa intendo e qual è il problema attuale. Ho usato semplice List invece di connettermi effettivamente al database, perché è solo un esempio per mostrare quello che sto cercando e sono aperto a cambiare e riscrivere completamente se qualcuno ha più conoscenza di questo tipo di problemi. / p>

Servizio di esempio:

public class CustomerService : ICustomerService
{
    public int AddCustomer(Customer customer)
    {
        using (var uof = new UnitOfWork())
        {
            var customerId = uof.CustomerRepository.Add(customer);
            var customerAddressId = uof.CustomerAddressRepository.Add(customer.Address);
            var customerAddress = uof.CustomerAddressRepository.Get(customerAddressId);
            customer.CustomerAddressId = customerAddressId;
            customer.Address = customerAddress;
            uof.CustomerRepository.Update(customer);
            uof.Commit();

            return customerId;
        }
    }

    public void UpdateCustomer(Customer customer)
    {
        using (var uof = new UnitOfWork())
        {
            uof.CustomerRepository.Update(customer);
            uof.Commit();
        }
    }

    public Customer GetCustomerById(int customerId)
    {
        using (var uof = new UnitOfWork())
        {
            return uof.CustomerRepository.Get(customerId);
        }
    }
}

UnitOfWork:

 public class UnitOfWork : IUnitOfWork, IDisposable
 {
    public ICustomerRepository CustomerRepository { get; }
    public ICustomerAddressRepository CustomerAddressRepository { get; }
    private SqlConnection _sqlConnection;
    private SqlTransaction _transaction;

    public UnitOfWork()
    {
        _sqlConnection = new SqlConnection();
        _transaction = _sqlConnection.BeginTransaction();

        CustomerRepository = new CustomerRepository(_sqlConnection);
        CustomerAddressRepository = new CustomerAddressRepository(_sqlConnection);
    }

    public void Commit()
    {
        _transaction.Commit();
    }

    public void Dispose()
    {
        _sqlConnection.Dispose();
    }
}

Repository di esempio:

public class CustomerRepository : ICustomerRepository
{
    private readonly SqlConnection _connection;
    private readonly List<Customer> _customers;

    public CustomerRepository(SqlConnection connection)
    {
        _connection = connection;
        _customers = new List<Customer>();
    }

    public int Add(Customer customer)
    {
        var id = _customers.Count + 1;
        customer.CustomerId = id;
        _customers.Add(customer);
        return id;
    }

    public void Update(Customer newCustomer)
    {
        var index = _customers.FindIndex(f => f.CustomerId == newCustomer.CustomerId);
        _customers.RemoveAt(index);
        _customers.Add(newCustomer);
    }

    public Customer Get(int id)
    {
        return _customers.FirstOrDefault(f => f.CustomerId == id);
    }
}

Come i controllori (codebehind corrente) usano questo:

var customer = _customerService.GetCustomerById(1);
var specificCustomers = _customerService.GetCustomersByIds(new int[] {5, 63, 75});

Con l'approccio attuale non posso prendere in giro ICustomerRepository nella mia classe UnitOfWork . Anch'io ho una serie di schemi misti e ciò di cui ho bisogno insieme, quindi non sono sicuro se questo è il modo per andare qui. Cosa posso fare per creare repository in UnitOfWork testabile?

    
posta FCin 08.11.2017 - 10:05
fonte

1 risposta

6

Questo è un problema comune nei test. Hai una classe A che usa la classe B che usa la classe C. Quindi come fai test classe B?

Devi essere in grado di prendere in giro la classe C e passarla alla classe B prima di poter provare B. Nel tuo caso, stiamo parlando di UnitOfWork a seconda di ICustomerRepository . Il trucco è questo: se UnitOfWork è responsabile per l'istanziazione di un'istanza di ICustomerRepository , non puoi mai sperare di simulare ICustomerRepository .

Puoi risolvere questo problema in due modi:

Tramite costruttore

Un'istanza di ICustomerRepository viene passata direttamente a UnitOfWork . In altre parole, la relazione tra UnitOfWork e ICustomerRepository cambia da " UnitOfWork è proprietaria di ICustomerRepository " a " UnitOfWork utilizza ICustomerRepository ". Questo è un posto molto migliore, e come regola generale, se stai usando un'interfaccia, dovresti sempre chiederti se la classe che istanzia l'implementazione dovrebbe farlo (più spesso che non è così).

La creazione effettiva di ICustomerRepository viene eseguita al di fuori di UnitOfWork e inoltrata. Quando esegui il test, passi invece la versione simulata di ICustomerRepository invece che non si connette al database ma agisce come ci si aspetterebbe da un'istanza di ICustomerRepository a comportarsi.

Via proprietà

La tua seconda opzione è aggiungere un setter alla tua proprietà CustomerRepository . UnitOfWork crea ancora un'istanza di CustomerRepository nel costruttore, ma sostituisci il tuo dopo il costruttore.

Questa opzione è meno piacevole perché rimane il proprietario di ICustomerRepository , e tuttavia aggiungere un setter implica UnitOfWork solo utilizza ICustomerRepository . Oltre a questo, qualsiasi logica di business che viene eseguita nel tuo costruttore deve essere eseguita anche sull'istanza di simulazione ICustomerRepository prima di essere passata, il che è ovviamente brutto.

Tuttavia, ha il vantaggio che si comporta come dovrebbe a meno che non venga sovrascritto, il che significa che il tuo codice rimane intatto tranne il nuovo setter e la tua classe di test aggiunge semplicemente un'istanza di simulazione di ICustomerRepository .

In alternativa non puoi istanziare ICustomerRepository in UnitOfWork e lasciare il setter, ma generalmente non mi piace l'idea di dipendere dall'impostazione delle proprietà affinché una classe funzioni correttamente.

Tramite un'interfaccia IRepositoryFactory

public interface IRepositoryFactory 
{
    ICustomerRepository GetCustomerRepository();
}

public class RepositoryFactory : IRepositoryFactory
{
    private SqlConnection connection;

    public RepositoryFactory(SqlConnection connection) 
    {
        this.connection = connection;
    }

    public ICustomerRepository GetCustomerRepository() 
    {
        return new CustomerRepository(this.connection);
    }
}

public class MockRepositoryFactory : IRepositoryFactory
{
    public MockRepositoryFactory() 
    {
    }

    public ICustomerRepository GetCustomerRepository() 
    {
        return new MockCustomerRepository();
    }
}

Non so chi crea un'istanza di UnitOfWork ma a questo punto avresti passato la tua istanza di IRepository e chiamerebbe getCustomerRepository() per ottenere il ICustomerRepository da passare a UnitOfWork .

Suppongo a questo punto che IRepositoryFactory venga caricato dinamicamente con la tua applicazione. Una volta creata l'istanza, non devi preoccuparti di esporre un'istanza di SqlConnection al chiamante delle classi del repository. Questo ti dà anche l'astrazione di cui hai bisogno per poter cambiare l'implementazione di IRepositoryFactory necessaria per eseguire i tuoi test.

Il punto centrale di queste astrazioni è di essere in grado di cambiare facilmente l'implementazione. Allo stato attuale, la modifica dell'implementazione non viene eseguita facilmente e pertanto testing le implementazioni dette sono difficili da unire strettamente.

    
risposta data 08.11.2017 - 12:24
fonte