Hace unos pocos dias, revisando una base de código me encontré con un uso “diferente” de los inicializadores a como yo los había usado anteriormente. Concretamente, la linea(s) de código que me llamaron la atención fueron las siguientes:
1 |
Database.SetInitializer(<span style="color: #0000ff">new</span> OrmViewRepositoryInitializer(<span style="color: #0000ff">new</span> OrmRepositoryInitializer(<span style="color: #0000ff">new</span> DropCreateDatabaseIfModelChanges<OrmRepository>()))); |
Un inicializador, que está decorado con otro inicializador??? Que raro, parece una complicación excesiva ¿no?. El caso es que, revisando su uso, se puso encima de la mesa que este decorador estaba creado para poder mantener un inicializador compartido, datos referenciales entre los juegos de pruebas y el producto. En definitiva, básicamente, el objetivo de esto es no tener que escribir dos seed de datos diferentes para los inicializadores de pruebas asumiendo que, generalmente, en tests se tiene un DropCreateDatabaseAlways y en producto un DropCreateDatabaseIfModelChange. Ummm, puedo llegar a entenderlo, sin embargo, se me plantean una seria de dudas con respecto a esto. ¿De verdad es necesario compartir datos referenciales entre el juego de tests y la base de datos de producción? ¿Si tengo un seed compartido, es esta la forma más simple de hacerla?
¿De verdad es necesario compartir datos referenciales entre el juego de tests y la base de datos de producción?
Para ser sinceros yo creo que no, por lo menos en lo que yo entiendo que son datos referenciales. ¿De verdad necesito tener toda la coleccion de paises, provincias, clientes por defecto etc etc etc.. para mis juegos de prueba? La experiencia me dice que no, de hecho, la experiencia me dice que generalmente los datos referenciales no dan una cobertura muy buena a los casos que se te suelen presentar, por ello, para las pruebas, utilizas datos mucho más límites, por ejemplo pensamos en juegos de caracteres diferentes, expresiones regulares más complejas etc etc que no solemos tener en datos maestros que suelen representar en lineas generales la misma prueba y no ofrecen mucha cobertura.
Otra de las razones por la cual tener todos los datos referenciales en la base de datos de prueba no tiene porque ser buena idea tiene que ver con el coste. Generalmente, cuando testeamos elementos de EF sin utilizar un “In Memory ObjectSet” recurrimos a la utilización de un AssemblyInitialize, o su correspondiente sino usas MSTests, en nuestras pruebas, algo, similar a lo siguiente:
1 |
[AssemblyInitialize()] |
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">static</span> <span style="color: #0000ff">void</span> Initialize(TestContext testContext) |
1 |
{ |
1 |
Database.SetInitializer(<span style="color: #0000ff">new</span> DropCreateDatabaseAlways<OrderingUnitOfWork>()); |
1 |
} |
Con esto, básicamente, lo que estamos haciendo es configurar un inicializador para EF antes de que se ejecute cualquier prueba dentro del ensamblado de tests. Concrétamente, estamos haciendo que se borre y se recree la base de datos de forma automática. Cuando, en nuestras pruebas queremos tener un conjunto dado de datos entonces hacemos una pequeña variante, creándonos un inicializador como podría ser el siguiente:
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">class</span> TestsInitializer |
1 |
:DropCreateDatabaseAlways<OrderingUnitOfWork> |
1 |
{ |
1 |
<span style="color: #0000ff">protected</span> <span style="color: #0000ff">override</span> <span style="color: #0000ff">void</span> Seed(OrderingUnitOfWork context) |
1 |
{ |
1 |
<span style="color: #008000">//TODO add test data</span> |
1 |
} |
1 |
} |
NOTA: Tal y como se enseño aquí, una de las novedades que se introdujo en EF 4.3.x fué la aparición de una nueva sección de configuración donde podemos especificar nuestros inicializadores, con lo cual, el trabajo de utilizar un AssemblyInitialize podría ser sustituído por una entrada en el archivo de configuración. Puede obtener más información sobre esto aquí.
El problema de incluir todos los datos referenciales en nuestro TestsInitializer se presenta en forma de coste, si cada vez que ejecutamos la primera de las pruebas se borrará, creará y poblará la base de datos no queremos que esto consuma un excesivo tiempo, puesto que, como todos ya sabemos, si las pruebas son viscosas o tardan mucho en ejecutarse acaban por desesperar y no se ejecutan con la frecuencia que deberían ejecutarse.. Por lo tanto, parece buena idea el restrigir el seed de los datos a aquellos que sean de valor para las pruebas únicamente, realmente, no necesitamos datos superficiales.
Curiosamente, en la revisión de esa linea de código, en el inicializador base me encontré algo de código como el siguiente:
1 |
<span style="color: #0000ff">this</span>.innerInitializer.InitializeDatabase(context); |
1 |
  |
1 |
<span style="color: #0000ff">if</span> (!context.Database.SqlQuery<<span style="color: #0000ff">int</span>>(<span style="color: #006080">"SELECT object_id FROM sys.views WHERE object_id = OBJECT_ID(N'[dbo].[OrdersView]')"</span>).Any()) |
1 |
{ |
1 |
context.Database.ExecuteSqlCommand(<span style="color: #006080">@"CREATE VIEw |
1 |
[dbo].[OrdersView] |
1 |
AS |
1 |
SELECT |
1 |
rs.Id AS OrderId, |
1 |
rs.State as StateValue |
1 |
ers"</span>); |
…
Si nos fijamos, parece que al tener un inicializador base, fué dificil quitarse la tentación de escribir algo más que inicialización ¿verdad?. La creación de estas vistas no es data-motion sino que pertenece al esquema y como tal debería de ser tratado. Para ello, en EF tenemos ( incluida directamente en el paquete) desde la version 4.3, actualmente 4.3.1, la posibilidad de utilizar migraciones, elementos sobre los que ya he hablado aquí y aquí en este mismo blog. Como todos sabemos, las aplicaciones cambian, los esquemas cambian y por lo tanto las vistas también, incluir esto junto a nuestras migraciones nos hará todo un poco más sencillo, puesto que aquí se aplican los mismos argumentos que con el resto de elementos de nuestra base de datos. Por lo tanto, en nuestros métodos Up o Down podríamos hacer de una forma sencilla el trabajo de crear/eliminar/modificar una vista por medio del método Sql de nuestras clases DbMigration.
1 |
Sql("CREATE VIEW... |
¿Si tengo un seed compartido, es esta la forma más simple de hacerla?
Bien, supongamos que por lo que sea decidimos que los argumentos anteriores no son válidos y necesitamos/queremos tener un seed compartido. Podemos hacer esto y mantener separados data-motion de la creación / manipulación de nuestros elementos de esquema? Bueno, vamos a intentarlo, si nos fijamos en nuestros elementos de trabajo IDatabaseInitializer<TContext> y DbMigrationsConfiguration<TContext> ámbos tienen en común la misma firma para el método de seed de datos.
1 |
<span style="color: #0000ff">protected</span> <span style="color: #0000ff">virtual</span> <span style="color: #0000ff">void</span> Seed(TContext context); |
Por lo tanto, en ámbos casos disponemos de una unidad de trabajo sobre la cual podremos interactuar, por lo tanto podríamos hacer algo como lo siguiente, aunque sea muy simple:
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">static</span> <span style="color: #0000ff">class</span> DataMotion |
1 |
{ |
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">static</span> <span style="color: #0000ff">void</span> Publish(OrderingUnitOfWork unitOfWork) |
1 |
{ |
1 |
unitOfWork.Orders.AddOrUpdate<Order>(<span style="color: #0000ff">new</span> Order[]{ |
1 |
<span style="color: #0000ff">new</span> Order(){OrderDate = DateTime.Now.AddDays(-1),Total =100M}, |
1 |
<span style="color: #0000ff">new</span> Order(){OrderDate = DateTime.Now.AddDays(-2),Total =300M}, |
1 |
<span style="color: #0000ff">new</span> Order(){OrderDate = DateTime.Now.AddDays(-3),Total =0M}, |
1 |
}); |
1 |
} |
1 |
} |
y utilizarlo tanto en nuestra configuración de migraciones como en nuestros inicializadores..
1 |
<span style="color: #0000ff">internal</span> <span style="color: #0000ff">sealed</span> <span style="color: #0000ff">class</span> Configuration : DbMigrationsConfiguration<OrderingUnitOfWork> |
1 |
{ |
1 |
<span style="color: #0000ff">public</span> Configuration() |
1 |
{ |
1 |
AutomaticMigrationsEnabled = <span style="color: #0000ff">false</span>; |
1 |
} |
1 |
  |
1 |
<span style="color: #0000ff">protected</span> <span style="color: #0000ff">override</span> <span style="color: #0000ff">void</span> Seed(OrderingUnitOfWork context) |
1 |
{ |
1 |
DataMotion.Publish(context); |
1 |
} |
1 |
} |
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">class</span> TestsInitializer |
1 |
:DropCreateDatabaseAlways<OrderingUnitOfWork> |
1 |
{ |
1 |
<span style="color: #0000ff">protected</span> <span style="color: #0000ff">override</span> <span style="color: #0000ff">void</span> Seed(OrderingUnitOfWork context) |
1 |
{ |
1 |
DataMotion.Publish(context); |
1 |
  |
1 |
<span style="color: #008000">//Add more data for specific tests</span> |
1 |
} |
1 |
} |
Conclusiones
Tal y como he comentado anteriormente, en mi opinión, los juegos de datos para testing son diferentes de los datos que solemos publicar junto con nuestro esquema, por ello,es conveniente separar entre inicializadores y migraciones los datos que se publicarán, de hecho, generalmente las bases de datos son diferentes ( en nombre y/o instancia ). Con la llegada de Migrations, ahora, en EF tenemos una forma simple de gestionar el ciclo de vida de nuestros esquemas, cambios, bajadas de versiones y también Data Motion.
Saludos
Unai
En este mismo blog ya he hablado mucho con respecto a las migraciones y los inicializadores de Entity