Quindi credo che il vostro punto principale è testabilità del codice, isn vero? In tal caso, si dovrebbe iniziare contando le responsabilità del metodo che si desidera testare e il refactoring del codice utilizzando un modello di responsabilità singola.
Il tuo codice di esempio ha almeno tre responsabilità:
- Creazione di un oggetto è una responsabilità - contesto è un oggetto. Inoltre è ed oggetto che non vuoi usare nel tuo test unitario, quindi devi spostare la sua creazione altrove.
- L'esecuzione della query è una responsabilità. Inoltre è una responsabilità che vorresti evitare nel tuo test unitario.
- Facendo qualche logica di business è una responsabilità
Per semplificare il test si dovrebbe refactoring del codice e dividere queste responsabilità a metodi separati.
public class MyBLClass()
{
public void MyBLMethod(int userId)
{
using (IMyContext entities = GetContext())
{
User user = GetUserFromDb(entities, userId);
// Some BL Code here
}
}
protected virtual IMyContext GetContext()
{
return new MyDbContext();
}
protected virtual User GetUserFromDb(IMyDbContext entities, int userId)
{
return entities.Users.Find(userId);
}
}
Ora unità logica di business di test dovrebbe essere pezzo di torta, perché il vostro test di unità può ereditare il metodo di classe e metodo factory falso contesto e l'esecuzione di query e diventare completamente indipendente EF.
// NUnit unit test
[TestFixture]
public class MyBLClassTest : MyBLClass
{
private class FakeContext : IMyContext
{
// Create just empty implementation of context interface
}
private User _testUser;
[Test]
public void MyBLMethod_DoSomething()
{
// Test setup
int id = 10;
_testUser = new User
{
Id = id,
// rest is your expected test data - that is what faking is about
// faked method returns simply data your test method expects
};
// Execution of method under test
MyBLMethod(id);
// Test validation
// Assert something you expect to happen on _testUser instance
// inside MyBLMethod
}
protected override IMyContext GetContext()
{
return new FakeContext();
}
protected override User GetUserFromDb(IMyContext context, int userId)
{
return _testUser.Id == userId ? _testUser : null;
}
}
quando si aggiungono più metodi e l'applicazione cresce si refactoring quei metodi di esecuzione delle query e il metodo contesto fabbrica per classi separate di seguire un'unica responsabilità sulle classi, come pure - si otterrà fabbrica contesto e sia un po 'di fornitore di query o in alcuni casi repository (ma tale repository non restituirà mai IQueryable
o ottiene Expression
come parametro in nessuno dei suoi metodi). Ciò ti consentirà anche di seguire il principio ASCIUTO in cui la tua creazione del contesto e le query più comunemente utilizzate saranno definite solo una volta in un punto centrale.
Così, alla fine si può avere qualcosa di simile:
public class MyBLClass()
{
private IContextFactory _contextFactory;
private IUserQueryProvider _userProvider;
public MyBLClass(IContextFactory contextFactory, IUserQueryProvider userProvider)
{
_contextFactory = contextFactory;
_userProvider = userProvider;
}
public void MyBLMethod(int userId)
{
using (IMyContext entities = _contextFactory.GetContext())
{
User user = _userProvider.GetSingle(entities, userId);
// Some BL Code here
}
}
}
Dove queste interfacce sarà simile:
public interface IContextFactory
{
IMyContext GetContext();
}
public class MyContextFactory : IContextFactory
{
public IMyContext GetContext()
{
// Here belongs any logic necessary to create context
// If you for example want to cache context per HTTP request
// you can implement logic here.
return new MyDbContext();
}
}
e
public interface IUserQueryProvider
{
User GetUser(int userId);
// Any other reusable queries for user entities
// Non of queries returns IQueryable or accepts Expression as parameter
// For example: IEnumerable<User> GetActiveUsers();
}
public class MyUserQueryProvider : IUserQueryProvider
{
public User GetUser(IMyContext context, int userId)
{
return context.Users.Find(userId);
}
// Implementation of other queries
// Only inside query implementations you can use extension methods on IQueryable
}
Il test sarà ora solo per uso falsi per contesto fabbrica e provider di query.
// NUnit + Moq unit test
[TestFixture]
public class MyBLClassTest
{
private class FakeContext : IMyContext
{
// Create just empty implementation of context interface
}
[Test]
public void MyBLMethod_DoSomething()
{
// Test setup
int id = 10;
var user = new User
{
Id = id,
// rest is your expected test data - that is what faking is about
// faked method returns simply data your test method expects
};
var contextFactory = new Mock<IContextFactory>();
contextFactory.Setup(f => f.GetContext()).Returns(new FakeContext());
var queryProvider = new Mock<IUserQueryProvider>();
queryProvider.Setup(f => f.GetUser(It.IsAny<IContextFactory>(), id)).Returns(user);
// Execution of method under test
var myBLClass = new MyBLClass(contextFactory.Object, queryProvider.Object);
myBLClass.MyBLMethod(id);
// Test validation
// Assert something you expect to happen on user instance
// inside MyBLMethod
}
}
Sarebbe po 'diverso in caso di repository che dovrebbe avere riferimento al contesto passato al suo costruttore prima di iniettarlo al vostro business class. La business class può ancora definire alcune query che non sono mai utilizzate in altre classi - quelle query probabilmente fanno parte della sua logica. È inoltre possibile utilizzare i metodi di estensione per definire alcune parti riutilizzabili delle query, ma è necessario utilizzare sempre tali metodi di estensione al di fuori della logica aziendale principale che si desidera analizzare unitamente (nei metodi di esecuzione della query o nel provider/repository di query). Ciò ti consentirà di semplificare il falso provider di query o i metodi di esecuzione delle query.
Ho visto your previous question e ho pensato di scrivere un post sul blog su quell'argomento, ma il nocciolo della mia opinione sui test con EF è in questa risposta.
Edit:
repository è argomento diverso che non si riferisce alla tua domanda iniziale. Il repository specifico è ancora valido. Non siamo contro i repository, we are against generic repositories perché non forniscono funzionalità aggiuntive e non risolvono alcun problema.
Il problema è che il repository da solo non risolve nulla. Ci sono tre schemi che devono essere usati insieme per formare un'astrazione appropriata: Repository, Unit of Work e Specifications. Tutti e tre sono già disponibili in EF: DbSet/ObjectSet come repository, DbContext/ObjectContext come Unit of works e Linq to Entities come specifiche. Il problema principale con l'implementazione personalizzata di repository generici menzionati ovunque è che sostituiscono solo repository e unità di lavoro con implementazione personalizzata ma dipendono ancora dalle specifiche originali => l'astrazione è incompleta e perde nei test in cui il repository fasullo si comporta allo stesso modo di insieme falso/contesto.
Lo svantaggio principale del mio provider di query è il metodo esplicito per qualsiasi query che sarà necessario eseguire. Nel caso del repository non si disporrà di tali metodi, si avranno solo pochi metodi che accettano le specifiche (ma di nuovo queste specifiche dovrebbero essere definite nel principio DRY) che costruirà condizioni di filtraggio delle query, ordini, ecc.
public interface IUserRepository
{
User Find(int userId);
IEnumerable<User> FindAll(ISpecification spec);
}
La discussione di questo argomento è ben oltre lo scopo di questa domanda e richiede di eseguire alcuni studi autonomi.
Btw. il mocking e il finging hanno uno scopo diverso: si simula una chiamata se è necessario ottenere dati di test dal metodo nella dipendenza e si prende in giro la chiamata se è necessario affermare che il metodo sulla dipendenza è stato chiamato con gli argomenti previsti.
Sono così felice che tu sia quello che ha risposto, come sembri essere il "Go-to-guy" per quanto riguarda EF Testability. Ho un paio di domande: hai detto ** riceverai il context factory e un qualche provider di query o in alcuni casi repository (ma quel repository non restituirà mai IQueryable o otterrà Expression come parametro in nessuno dei suoi metodi). Ciò ti consentirà anche di seguire il principio DRY in cui la creazione del tuo contesto e le query più comunemente utilizzate verranno definite solo una volta in un punto centrale. ** - Ho pensato che fossi totalmente contrario all'uso del repository. Potete per favore chiarire con un esempio? –
Puoi anche chiarire questa frase: ** 'Sarebbe un po' diverso nel caso di repository che dovrebbe avere riferimento al contesto passato al suo costruttore prima di iniettarlo nella tua classe business. '** (un esempio di un repository di questo tipo e quando si dovrebbe usarlo sarebbe molto apprezzato) –
e infine - l'ultimo blocco che hai scritto: ** La tua business class può ancora definire alcune query che non sono mai usate in altre classi - quelle query sono probabilmente parte di della sua logica. ** - Il tuo primo esempio di codice ha usato questa tecnica, giusto? quando dovrei usare quello e quando dovrei usare i metodi di estensione? dove vanno questi metodi di estensione? –