Tests de integración con Entity Framework (I): Preparando el entorno

En mi último post explicaba los conceptos básicos para escribir tests de integración contra una base de datos, pero me quedaba en eso, conceptos, y aunque siempre insistiré en lo importante que es conocer las bases, un ejemplo siempre viene bien para dejar más claro cómo aplicar la teoría al mundo real.

En los próximos posts vamos a ver un ejemplo completo de cómo podemos escribir tests de integración contra una base de datos sin sufrir demasiado. Como tecnología de acceso a datos usaremos Entity Framework por emplear algo familiar a la mayoría de vosotros, aunque las ideas son aplicables a otros ORMs o, en general, a cualquier estrategia de persistencia.

OJO: Aunque llevo mucho tiempo trabajando con ORMs, no soy un experto en Entity Framework y hay partes de su filosofía que no me acaban de gustar, por lo que puede que el código resulte un poco extraño o que, directamente, esté mal. Estaré encantado de recibir correcciones en los comentarios.

El modelo

Lo primero que necesitamos para nuestro ejemplo es tener un modelo sobre el que realizar las pruebas. En este caso, vamos a utilizar un modelo muy simple sobre películas, directores y países:

Modelo de objetos

Tenemos películas (Movie) dirigidas por personas (Person) y producida por uno o más países (Country). Además, cada película tendrá el año de estreno y la recaudación obtenida. Es un modelo sencillo pero que cuenta con un par de relaciones para luego poder lanzar alguna consulta medianamente interesante.

El código del modelo es más o menos así:

public class Country
{
  protected Country() { /*EF*/ }

  public Country(string name)
  {
    Check.Require(!string.IsNullOrWhiteSpace(name), "Name is required");

    Name = name;
  }

  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Person
{
  protected Person() {  /*EF*/ }

  public Person(string name)
  {
    Check.Require(!string.IsNullOrWhiteSpace(name), "Name is required");
    Name = name;
  }

  public int Id { get; private set; }
  public string Name { get; private set; }
}
 
public class Movie
{
  private decimal gross;

  protected Movie() { /*EF*/ }

  public Movie(string title, int year, Person director, params Country[] countries)
  {
    Check.Require(!string.IsNullOrWhiteSpace(title), "Title is required");
    Check.Require(year > 1850, "No film was recorded prior to 1850");
    Check.Require(director != null, "Someone had to direct the film");
    Check.Require(countries != null && countries.Length > 0, "Al least one country is required");

    Title = title;
    Year = year;
    Director = director;
    Countries = countries.ToList();
  }

  public int Id { get; private set; }
  public string Title { get; private set; }
  public int Year { get; private set; }
  public virtual Person Director { get; private set; }
  public virtual ICollection<Country> Countries { get; private set; }

  public decimal Gross
  {
    get { return gross; }
    set
    {
      Check.Require(value >= 0, "Gross must be a positive number");
      gross = value;
    }
  }
}

No es que sea un modelo muy rico, pero al menos intenta proteger sus invariantes (dentro de las serias limitaciones que impone Entity Framework). Esto hará que luego a la hora de escribir tests tengamos que tener cierto cuidado con la forma en que creamos las entidades y nos permitirá ver técnicas para mantener los tests legibles y focalizados en la información que nos interesa sin tener que pervertir nuestro modelo para ello.

El DbContext que usaremos para trabajar con el modelo es éste:

public class MovieDbContext : DbContext
{
  public MovieDbContext(string dbNameOrConnectionString)
    : base(dbNameOrConnectionString)
  {
    // No quiero que se creen mágicamente bases de datos
    Database.SetInitializer<MovieDbContext>(null);
  }

  public DbSet<Movie> Movies { get; set; }
  public DbSet<Person> Companies { get; set; }
  public DbSet<Country> Countries { get; set; }

  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<Movie>()
      .HasMany(x => x.Countries)
      .WithMany()
      .Map(x =>
      {
        x.ToTable("MovieCountries");
        x.MapLeftKey("MovieId");
        x.MapRightKey("CountryId");
      });
  }
}

Lo único reseñable es que he desactivado la creación por defecto de la base de dato que hace Entity Framework porque no me gusta la idea de que se puedan ir creando bases de datos sin hacerlo explícitamente.

Las herramientas básicas

En el post anterior decía que para poder testear una base de datos, uno de los requisitos era poder crear cómodamente una base de datos limpia y poder devolverla a ese estado. Por suerte, hacer eso con Entity Framework es muy sencillo a través de los DbInitializers.

Crear una base de datos nueva es tan sencillo como ejecutar el siguiente código:

using (var context = new MovieDbContext("moviedb")
{
  new DropCreateDatabaseAlways<MovieDbContext>()
    .InitializeDatabase(context);
}

Para limpiar la base de datos, podemos recurrir a la estrategia de enumerar las tablas e irlas borrando de manera ordenada teniendo en cuenta sus relaciones. El código es un poco largo para ponerlo aquí, pero podréis encontrarlo en los fuentes del proyecto dentro de la clase DbCleaner.

Todo esto, podemos encapsularlo en una clase con métodos de utilidad:

public static class DbTools
{
  public static void CreateNewDatabase(MovieDbContext context)
  {
    new DropCreateDatabaseAlways<MovieDbContext>()
      .InitializeDatabase(context);
  }

  public static void CleanDatabase(MovieDbContext context)
  {
    new DbCleaner(context).DeleteAllData();
  }

  public static void LoadSampleData(MovieDbContext context)
  {
    new SampleDataLoader(context).LoadData();
  }
}

De paso, he añadido un pequeño cargador de datos de ejemplo (muy útil durante el desarrollo para poder hacer pruebas manuales).

Aprovechando que tenemos este conjunto de utilidades, podemos montar una aplicación de consola muy sencilla (aquí tenéis el código) que nos ayude a realizar varias tareas:

// Crear una base de datos nueva vacía
c:\tmp> efcmd --new moviedb

// Limpiar una base de datos existente
c:\tmp> efcmd --clean moviedb

// Crear una base de datos nueva e inicializarla con datos de ejemmplo
c:\tmp> efcmd --sample moviedb

Esta aplicación no la usaremos directamente en los tests, pero ya he hablado alguna vez de lo interesante que resulta tener este tipo de aplicaciones auxiliares para simplificarnos las tareas que realizamos habitualmente durante el desarrollo.

Si os fijáis, la complejidad de estas herramientas no depende de la complejidad del modelo, es decir, aunque aquí tengamos un modelo muy simple, en un escenario real con docenas de entidades y tablas podríamos usar exactamente las mismas herramientas que estamos usando ahora. Esto hace que la inversión inicial en desarrollarlas (si es que se puede considerar desarrollar lo que hemos hecho) se amortice rápidamente y sea fácil de reutilizar en distintos proyectos.

Resumen

De momento no hemos escrito ningún test, pero ya tenemos todo preparado para empezar a trabajar. Tenemos un modelo de objetos, tenemos nuestra implementación del acceso a datos a través de Entity Framework y, lo que es más importante, tenemos herramientas para crear una base de datos limpia y para resetear una base de datos a su estado inicial.

Puede que no parezca gran cosa, a fin de cuentas lo único que hemos hecho ha sido organizar un poco lo que ya nos ofrece Entity Framework, pero el mero hecho de hacerlo explícito ya es un avance importante.

Si queréis ver con más detalle cómo está todo montado, podéis echar un vistazo al código del ejemplo.

En el siguiente post veremos cómo podemos estructurar los tests que interactúan con la base de datos para que resulten sencillos de escribir y sean sólidos frente a cambios en nuestro modelo o incluso en nuestra tecnología de acceso a datos.