2015-01-25 19 views
7

Ich verwende ein generisches Repository-Muster Repository<TEntity>, in dem Repositorys über einen Kontext auf die Entitäten zugreifen. Dann habe ich eine Service-Schicht, die einen Context im Konstruktor akzeptiert. Jetzt kann ich mehrere Repositories in einem Service haben und über denselben Kontext auf die Entitäten zugreifen. Ziemlich Standard. Dies ist perfekt für Tabellen/Ansichten, die Entitäten zugeordnet sind, aber ich kann keine Testdaten verarbeiten, die gespeicherte Prozeduren durchlaufen.Spionage- und Komponententest Gespeicherte Prozeduren in EF

Dies ist meine aktuelle Setup:

IDbContext:

public interface IDbContext : IDisposable 
{ 
    IDbSet<T> Set<T>() where T : class; 

    DbEntityEntry<T> Entry<T>(T entity) where T : class; 

    void SetModified(object entity); 

    int SaveChanges(); 

    // Added to be able to execute stored procedures 
    System.Data.Entity.Database Database { get; } 
} 

Kontext:

public class AppDataContext : DbContext, IDbContext 
{ 
    public AppDataContext() 
     : base("Name=CONNECTIONSTRING") 
    { 
     base.Configuration.ProxyCreationEnabled = false; 
    } 

    public new IDbSet<T> Set<T>() where T : class 
    { 
     return base.Set<T>(); 
    } 


    public void SetModified(object entity) 
    { 
     Entry(entity).State = EntityState.Modified; 
    } 

    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    { 
     modelBuilder.Configurations.Add(new BookingMap()); 
    } 

    // Added to be able to execute stored procedures 
    System.Data.Entity.Database Database { get { return base.Database; } } 
} 

Generisches Repository:

public class Repository<T> : IRepository<T> where T : class 
{ 
    private readonly IDbContext context; 

    public Repository(IDbContext context) 
    { 
     this.context = context; 
    } 

    public IQueryable<T> GetAll() 
    { 
     return this.context.Set<T>().AsQueryable(); 
    } 

    public void Add(T entity) 
    { 
     this.context.Set<T>().Add(entity); 
    } 

    public void Delete(T entity) 
    { 
     this.context.Set<T>().Remove(entity); 
    } 

    public void DeleteAll(IEnumerable<T> entities) 
    { 
     foreach (var e in entities.ToList()) 
     { 
      this.context.Set<T>().Remove(e); 
     } 
    } 

    public void Update(T entity) 
    { 
     this.context.Set<T>().Attach(entity); 
     this.context.SetModified(entity); 
    } 

    public void SaveChanges() 
    { 
     this.context.SaveChanges(); 
    } 

    public void Dispose() 
    { 
     if (this.context != null) 
     { 
      this.context.Dispose(); 
     } 
    } 
} 

Service:

public class BookingService 
{ 
    IDbContext _context; 

    IRepository<Booking> _bookingRepository; 

    public BookingService(IDbContext context) 
    { 
     _context = context; 

     _bookingRepository = new Repository<Booking>(context); 
    } 

    public IEnumerable<Booking> GetAllBookingsForName(string name) 
    { 
     return (from b in _bookingRepository.GetAll() 
       where b.Name == name 
       select b); 
    } 
} 

Test:

[TestClass] 
public class BookingServiceTest 
{ 
    [TestMethod] 
    public void Test_Get_All_Bookings_For_Name() 
    { 
     var mock = new Mock<IDbContext>(); 
     mock.Setup(x => x.Set<Booking>()) 
      .Returns(new FakeDbSet<Booking> 
      { 
       new Booking { Name = "Foo" }, 
       new Booking { Name = "Bar" } 
      }); 

     BookingService _bookingService = new BookingService(mock.Object); 

     var bookings = _bookingService.GetAllBookingsForName(name); 

     Assert.AreEqual(2, bookings.Count(), "Booking count is not correct"); 
    } 
} 

Dies ist ideal für Tabellen/Ansichten, die Entitäten abzubilden, aber ich kann nicht Unit-Test-Daten über gespeicherte Prozeduren kommen.

Ich schaute auf das Internet und fand DbContext.Database Eigenschaft und ich bin in der Lage, gespeicherte Prozeduren mit der .SqlQuery() Funktion auszuführen und sie auf einen Entitätstyp abzubilden.

Dies ist, was ich in die Repository<T> Klasse hinzugefügt:

public IEnumerable<T> SqlQuery(string storedProc, params object[] paramList) 
{ 
    return this.context.Database.SqlQuery<T>(storedProc, paramList); 
} 

Und die .SqlQuery() Funktion in meiner Dienstklasse nennen:

public IEnumerable<Booking> GetAllBookings(string name) 
{ 
    return _bookingRepository.SqlQuery("EXEC GetAllBookings @name = {0}", name); 
} 

Dieses große Werk (ich bin in der Lage, einige Daten zu erhalten) Aber meine Frage ist, wie kann ich das simulieren und testen?

+0

Es ist sinnlos, eine gespeicherte Prozedur nachzuahmen. Wie würdest du wissen, dass die reale Sache wie erwartet läuft, wenn der Komponententest ein grünes Licht hat? Wenn Sie die * Daten * von einem Sproc-Unit-Test benötigen (nicht mit der Logik des Sprocs verbunden), können Sie einfach Mock-Daten erstellen. Um sprocs zu testen, führen Sie Integrationstests durch. –

+0

Genau, ich werde nur die * Daten * von den Sproc-to-Unit-Tests mit benötigen. Wie kann ich die Scheindaten erstellen? – Gaui

+0

Ich denke, zu Beginn sollten Sie Sprocs in Ihr Repository kapseln, Ein Dienst sollte nicht über SQL (oder irgendwelche Persistenz Details) wissen. –

Antwort

5

Sie können die Database Eigenschaft mit einigen Schnittstelle sagen IDatabase mit SqlQuery Methode weg abstrahieren.

interface IDatabase 
{ 
    public IEnumerable<T> SqlQuery<T>(string sql, params Object[] parameters); 
} 

class DatabaseWrapper : IDatabase 
{ 
    private readonly Database database; 
    public DatabaseWrapper(Database database) 
    { 
     this.database = database; 
    } 

    public IEnumerable<T> SqlQuery<T>(string sql, params Object[] parameters) 
    { 
     return database.SqlQuery<T>(storedProc, paramList); 
    } 
} 

Ändern Sie bitte Ihre IDbContext Schnittstelle IDatabase statt konkrete Instanz zu verwenden, so dass wir sie verspotten.

public interface IDbContext : IDisposable 
{ 
    ... 

    // Added to be able to execute stored procedures 
    IDatabase Database { get; } 
} 

und Ihre Implementierung auf diese Weise

public class AppDataContext : DbContext, IDbContext 
{ 
    private readonly IDatabase database; 
    public AppDataContext() 
     : base("Name=CONNECTIONSTRING") 
    { 
     base.Configuration.ProxyCreationEnabled = false; 
     this.database = new DatabaseWrapper(base.Database); 
    } 
    ... 

    // Added to be able to execute stored procedures 
    IDatabase Database { get { return database; } } 
} 

An dieser Stelle Ich glaube, Sie wissen, wie die IDatabase zu verspotten die Testdaten zurückzukehren.

+0

Vielen Dank für dieses detaillierte Beispiel. Das ist etwas, was ich mir vorgestellt habe. – Gaui

+0

'System.Data.Entity.Database' implementiert nicht 'IDatabase' (oder irgendeine Schnittstelle). Ich sehe nicht, wie das funktionieren könnte. * Vielleicht * 'IDbContext' könnte eine Methode' SqlQuery' haben (die echte implementiert als 'Database.SqlQuery'). Aber außerdem denke ich, dass dein letzter Satz sehr optimistisch ist. Es würde das Parsen von SQL-Anweisungen erfordern, was nicht trivial ist, selbst wenn es nur um einfache Sproc-Aufrufe geht. Es ist das SQL selbst, das abstrahiert werden sollte. –

+0

@GertArnold Ich bin mir nicht sicher, ob ich dich verstehe. Sie müssen dafür kein SQL analysieren. Wir verwenden nur etwas wie Adaptermuster; Wrapping der 'DbContext.Database', um hier 'this.database = new DatabaseWrapper (base.Database)' zu verbinden; und einfach die Arbeit an sich delegieren. Also ich bin mir nicht sicher, was du meinst. Ich nehme an, du hast meinen Code übersehen oder ich vermisse etwas, –

6

Ich habe gerade ein Bedürfnis, dies zu tun, und mein googling führte mich zu dieser Frage. Ich mochte die Antwort von Sriram Sakthivel nicht, ich wollte nicht noch eine Abstraktion einführen müssen, als ich bereits eine hatte:

Ich hatte bereits eine Schnittstelle, die ich aus meiner DbContext extrahiert und implementiert hatte in einem test double.

Ich habe einfach int ExecuteSqlCommand(string sql, params object[] parameters) meine Schnittstelle und im aktuellen Kontext es so I umgesetzt:

public int ExecuteSqlCommand(string sql, params object[] parameters) 
{ 
    return Database.ExecuteSqlCommand(sql, parameters); 
} 

die offensichtlich nur die Delegierten auf die tatsächliche EF Database Eigenschaft, um die Arbeit zu tun.

Und in meinem Test Doppel ich es wie folgt umgesetzt:

public int ExecuteSqlCommand(string sql, params object[] parameters) 
{ 
    return 0; 
} 

, die nicht wirklich etwas zu tun hat, was der Punkt ist: Sie sind nicht Gerät kann die aktuelle gespeicherte Prozedur zu testen, müssen Sie nur eine Möglichkeit, etwas Nützliches zurückzugeben.

Ich kann mir vorstellen, dass ich irgendwann etwas anderes als 0 in einem Komponententest zurückgeben muss. An diesem Punkt werde ich wahrscheinlich etwas wie einen Func<int> executeSqlCommandResultFactory Test für den Doppelkonstruktor einführen, damit ich ihn kontrollieren kann im Moment gilt YAGNI.

Verwandte Themen