June 2011 - Posts
Anterior Post
Estuve trabajando en mi projecto AjModel. Pueden bajar el avance del código desde mi AjCodeKatas Project dentro de trunk/AjModel. Agregué una clase Model, y mejoré EntityModel y PropertyModel:
El modelo referencia a todos los modelos de entidades que quiero tomar de mi modelo ya codificado. Recordemos: AjModel se basa y enriquece un modelo ya codificado de clases, permitiendo agregar información como descripciones para humanos, leyendas, etc. La idea es generar automáticamente la UI desde el modelo en código, para diferentes tecnologías. El modelo de AjModel permite establecer ese puente entre nuestro modelo en código y las distintas UI. El modelo enriquecido debería servir para esa función AUN cuando no se le agregue nada. Y servir de base para generar la UI en ejecución. El modelo enriquecido debe ser poder consumido por distintas tecnologías de UI: la aplicación “prueba de concepto” que incluyo en el código es ASP.NET MVC.
Como anticipaba en el post anterior, agregué una interfaz “fluent” a EntityModel y PropertyModel. Hay métodos de extensión, wrappers/builders, como:
Algunos ejemplos de uso, tomados de los tests:
EntityModel<Customer> model = new EntityModel<Customer>();
FluentEntityModel<Customer> fluentModel = new FluentEntityModel<Customer>(model);
fluentModel.Name("BusinessCustomer")
.SetName("BusinessCustomers")
.Descriptor("Business Customer")
.SetDescriptor("Business Customers");
//..
Model model = new Model();
var entityModel = model.ForEntity<Customer>();
entityModel.Descriptor("Business Customer")
.SetDescriptor("Business Customers");
//..
Model model = new Model();
model.ForEntity<Customer>().Property(
c => c.Name,
pm => pm.Descriptor("Customer Name")
.Description("The Customer Name")
);
donde pm es un FluentPropertyModel.
En el proceso de la UI, quiero recuperar una entidad por su identidad, para poder mostrar su detalle. Y también, quiero obtener una lista de entidades para alimentar una grilla. Entonces, necesito algo para manejar entidades, como una lista, y que se implemente por abajo en distintas tecnologías. La actual implementación se basa en una lista en memoria:
En mis planes está escribir ejemplos de adapter para NHibernate, Entity Framework, pero eso todavía está solo en papel y en mi mente ;-)
Ejemplos de uso:
this.entityModel = new EntityModel<Customer>();
this.domain = new SimpleDomain();
this.repository = new Repository<Customer>(this.entityModel, this.domain.Customers);
Noten que hay repositorios con tipo, usando generics. Y necesita para manejarse un EntityModel, y (en la actual implementación) un IList de entidades. El Context contiene una lista de todos los repositorios:
var entityModel = new EntityModel<Customer>();
var domain = new SimpleDomain();
var repository = new Repository<Customer>(entityModel, domain.Customers);
var context = new Context();
context.AddRepository(this.repository);
var repo = context.GetRepository("Customer");
SimpleDomain es una clase que creé para los tests:
public class SimpleDomain
{
public SimpleDomain()
{
this.Customers = new List<Customer>();
for (int k = 1; k <= 10; k++)
{
Customer customer = new Customer()
{
Id = k,
Name = string.Format("Customer {0}", k)
};
this.Customers.Add(customer);
}
}
public IList<Customer> Customers { get; set; }
public ProductList Products { get; set; }
}
Un modelo puede ser creado alimentándose de la estructura de una clase como SimpleDomain. Descubre las propiedades públicas que implementan IList (por ahora):
var model = new Model(typeof(SimpleDomain));
var entityModel = model.GetEntityModel("Customer");
Assert.IsNotNull(entityModel);
entityModel = model.GetEntityModel("Product");
Assert.IsNotNull(entityModel);
Planeo agregar automáticamente todos los tipos de un assembly que satisfagan un predicado, como tener “Entities” en su namespaces. new Model(IEnumerable<Type> types) debería bastar.
Las entidades (objetos) pueden ser agregadas y recuperadas usando un repositorio:
Customer entity = new Customer() { Id = 1000 };
this.repository.AddEntity(entity);
Customer newEntity = this.repository.GetEntity(1000);
Y pueden ser removidas, también.
El modelo de entidad tiene la información para crear nuevas entidades desde valores (en un diccionario con pares nombre/valor). Una capacidad que necesito para usar en la creación de entidades desde la UI:
EntityModel<Person> model = new EntityModel<Person>();
IDictionary<string, object> values = new Dictionary<string, object>()
{
{ "Id", 1 },
{ "FirstName", "Joe" },
{ "LastName", "Doe" },
{ "Age", "30" }
};
object entity = model.NewEntity(values);
En próximos posts, discutiré alguna implementación de la UI en MVC. Todo es “work in progress”, pero el proyecto ya va tomando forma. Y me divieggtto como loco ;-)
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Anterior Post
Próximo Post
Ahora ya tengo algunos tests y en verde. Voy a agregar algunas vistas MVC en este paso de hoy. Pueden bajar el código desde mi AjCodeKatas Google Project, en trunk/AppTdd/Step04. Así que este post tiene pocos nuevos tests: muestra que podemos agregar interfaz de usuario DESPUES de los tests, y (para destacar) SIN TENER una base de datos aún, ni siquiera una capa de servicios (“service layer”).
Primero, agregué una master page Site.master y un estilo style.css (que tomé de otro proyecto ASP.NET MVC 2)
Agregué un nuevo controlador Home:
Y usando el menú de contexto sobre el método Index agregué una vista asociada al método:
Modifiqué el código de la master page para tener una nueva opción en el menú:
Entonces, agregué una vista, esta vez asociada al método Index del controlador Subject:
Seleccioné a “List” como “Vien Content”, y a “Subject” como el tipo a usar como base del modelo.
Pero el método del controlador es:
public ActionResult Index()
{
return View(subjects);
}
Usa una lista de temas (“subjects”) que ha sido inyectada hasta ahora en un constructor por los tests:
public SubjectController(IList<Subject> subjects)
{
this.subjects = subjects;
}
Pero esperen! Esa lista es inyectada por los tests. Ahora, necesito que esté disponible en el proceso normal de un controlador. Así que agregué un constructor sin parámetros:
public SubjectController()
: this(Domain.Instance.Subjects)
{
}
El Domain.Instance es un dominio en memoria:
public class Domain
{
private static Domain instance = new Domain();
private Domain()
{
this.Subjects = new List<Subject>()
{
new Subject() { Id = 1, Name = "Mathematics" },
new Subject() { Id = 2, Name = "Physics" },
new Subject() { Id = 3, Name = "Biology" },
new Subject() { Id = 4, Name = "Literature" }
};
}
public static Domain Instance { get { return instance; } }
public IList<Subject> Subjects { get; set; }
}
Ahora sí el controlador de Subject puede manejar el método Index. Cambio enlaces en la vista que me agregó el wizard. Los cambio de:
a que usen la propiedad Id como clave primaria:
Este es el resultado:
Creé entonces otras vistas, para los otros métodos del controlador Subject. Seguí usando el wizard de Visual Studio “Add View … “:
Me encontré que me faltaban algunas acciones. Escribí una nueva:
public ActionResult Edit(int id)
{
var model = this.subjects.Where(s => s.Id == id).FirstOrDefault();
return View(model);
}
con tests! ;-)
[TestMethod]
public void GetSubjectForEdit()
{
IList<Subject> subjects = GetSubjects();
SubjectController controller = new SubjectController(subjects);
ActionResult result = controller.Edit(1);
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(ViewResult));
ViewResult viewResult = (ViewResult)result;
Assert.IsInstanceOfType(viewResult.ViewData.Model, typeof(Subject));
Subject model = (Subject)viewResult.ViewData.Model;
Assert.AreEqual(1, model.Id);
Assert.AreEqual("Mathematics", model.Name);
}
Curiosamente, detecté un test “malo”. Yo había escrito en test AddSubject:
Assert.IsTrue(subjects.Any(s => s.Name == "Chemistry"));
Assert.AreEqual(4, subject.Id);
Pero el nuevo Id debería dar 5 (mi lista usada en los tests tiene 4 temas predefinidos (“subjects”)):
Assert.IsTrue(subjects.Any(s => s.Name == "Chemistry"));
Assert.AreEqual(5, subject.Id);
Arreglé el código y la acción del controler, ejecuté la aplicación web, y voila! Todo funcionó.
Puntos principales:
- Estoy usando un dominio en memoria.
- Las vistas se ejecutan sobre las acciones YA probadas con tests
No hay capa lógica de servicios ni persistencia, aún. Vean que no son necesarias al principio. Voy a agregar una “service layer” como gran refactor. Otros próximos pasos: agregar más clases al dominio (libros en clase Book), escribir esa capa de servicio usando tests, “enchufarla” en los controladores. Y en algún momento agregar persistencia.
Pueden leer posts interesantes sobre TDD sin una base de datos (o agregándola después) escritos por @RonJeffries:
But We Need a Database … Don’t We? | xProgramming.com
See? We Don’t Need a Database … Yet | xProgramming.com
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Siguiente Post
Ya saben que soy un gran promotor de elevar el nivel de abstracción, separar los problemas de las soluciones, etc. Uno de mis proyectos, AjGenesis, está dedicado a usar modelos de libre definición que pueden producir, con generación de código, aplicaciones en diferentes plataformas y tecnologías. Ahora, quiero explorar otra manera de hacer las cosas: en lugar de generación de código, podemos armar el sistema en ejecución, habiendo enriquecido un modelo inicial.
Primeras Ideas
Es natural, en estos tiempos, tener un modelo (de dominio, de negocio, sea como lo hayan nombrado):
Podemos tener clases principales (entidades), asociaciones, colecciones, servicios con méetodos, etc. Y podemos armar esos artefactos usando TDD o no. Pero esos elementos SON el núcleo del sistema. Pero para poder usarlo en otros ámbitos (interfaz de usuario, persistencia), necesitamos enriquecer, extender el modelo. Un ejemplo de AjGenesis:
<Entity>
<Name>Customer</Name>
<Description>Customer</Description>
<SetName>Customers</SetName>
<Descriptor>Customer</Descriptor>
<SetDescriptor>Customers</SetDescriptor>
<SqlTable>customers</SqlTable>
<Properties>
<Property>
<Name>Id</Name>
<Description>Id</Description>
<Type>Id</Type>
<SqlType>int</SqlType>
</Property>
<Property>
<Name>Name</Name>
<Description>Name</Description>
<Type>Text</Type>
<SqlType>varchar(200)</SqlType>
<Required>true</Required>
</Property>
</Properties>
</Entity>
Este modelo abstracto tiene atributos como Name (el nombre de una entidad), SetName (el nombre de un conjunto de entidades) que podemos usar como nombres a usar en nuestro código a generar. Pero también tiene Descriptor, SetDescriptor, Description a ser usadas en la generación de la interfaz de usuario. Estos atributos adicionales son parte de lo que llamo “Modelo Enriquecido”: un modelo con propiedades, información adicional que nos ayudan a describir el modelo en términos humanos. Podría agregar datos adicionales (como tipos de SQL, longitud), a ser usado en persistencia, pero no es un objetivo en esta primera parte de mi experimiento:
Los Modelos de interfaz dependen de la tecnología a usar. El modelo extendido no depende de la tecnología. Una vez que tenemos el modelo extendido describiendo entidades, propiedades en detalle, entoncs los View Models pueden ser producidos en “runtime” (ejecución) para cada tecnología final. Y algo importante: si el “nombre humano” de una entidad o propiedad no está especificada, podemos usar el nombre de código. Esta es el dato “asumido” de un modelo extendido: usa el modelo, Luke! ;-)
En los próximos párrafos muestro código para extender el modelo. Pero siempre tengo como decisión clave de diseño: puedo generar toda la interfaz aún si no me indican nada adicional sobre el modelo inicial.
Algo de Código
Entonces, hace una semana comencé a codificar AjModel. Pueden ver mi progreso en mi AjCodeKatas Project en trunk/AjModel. Mis primeros modelos extendidos están compuestos de Entidades (los principales objetos del modelo) y Propiedades:
Estoy probando con una clase simple Customer:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string Notes { get; set; }
}
Un test de ejemplo:
[TestMethod]
public void GetProperties()
{
EntityModel model = new EntityModel(typeof(Customer));
var properties = model.Properties;
Assert.IsNotNull(properties);
Assert.AreEqual(4, properties.Count);
Assert.AreEqual("Id", properties.First().Name);
Assert.AreEqual("Name", properties.Skip(1).First().Name);
Assert.AreEqual("Address", properties.Skip(2).First().Name);
Assert.AreEqual("Notes", properties.Skip(3).First().Name);
}
Lo que viene: ServiceModel (describiendo servicios), y Model (conteniento modelos de entidades y servicios). Más propiedades en EntityModel, como .Descriptor, .SetDescriptor, .Description, etc.
Cada modelo de interfaz depende de la tecnología. Como prueba de concepto, estoy usando ASP.NET MVC, en el proyecto AjModel.WebMvc. Hay un EntityController, no genérico aún, dedicado a Customer por ahora:
public ActionResult Index()
{
var model = new EntityListViewModel();
model.Entities = Domain.Instance.Customers;
model.EntityModel = new EntityModel(typeof(Customer));
return View(model);
}
Domain.Instance.Customers es una lista en memoria de clientes (“customers”). Algo de código en la vista:
<h2>
<%= this.Model.Title %></h2>
<table>
<tr>
<% foreach (var property in this.Model.EntityModel.Properties)
{
%>
<th>
<%= Html.Encode(property.Name) %>
</th>
<%} %>
</tr>
<% foreach (var entity in this.Model.Entities)
{ %>
<tr>
<% foreach (var property in this.Model.EntityModel.Properties)
{ %>
<td><%= property.GetValue(entity) %></td>
<%} %>
</tr>
<%} %>
</table>
Próximos Pasos
Quiero tener una interfaz fluent como:
model.ForEntity<Customer>()
.Descriptor("Customer")
.SetDescriptor("Customers")
Y quiero usar Expression<Func…> para usar validación de código (define propiedades por código en vez de por un nombren en string como “Name”):
model.ForEntity<Customer>()
.ForProperty(c => c.Name)
.Description("Customer Name")
.IsRequired()
O algo como:
model.ForEntity<Customer>()
.Property(c => c.Name,
pm => pm.Description("Customer Name")
.IsRequired()
)
.Property(c => c.Address,
pm => pm.Description("Customer Address")
)
donde pm es un PropertyModel. .ForProperty retorna un PropertyModel con interfaz fluent. Pero .Property retorna un fluent sobre EntityModel, así que puedo usar el segundo parámetro pm => … como una forma de indicar qué quiero hacer con la propiedad, sin perder el modelo fluent de la entidad.
Pero no hay código de esto en el repositorio, todavía. Ya me conocen, estoy haciendo esto en mi tiempo libre, tengo que ir a trabajar, todavía necesito trabajar, la fortuna de la familia quedó en las mesas de Montecarlo ;-)
Generación de Código vs En Runtime
Yo sigo prefiriendo la generación de código desde un modelo de libre definición: marca una clara separación del problema de su posible solución tecnológico. Y puede ser fácilmente adaptado a los cambios en tecnología y lenguajes de programación (Java vs Scala vs C# vs … y lo que venga en el futuro.. ;-). Pero muchos desarrolladores prefieren trabajar directamente con código. Por eso este experimento. Desde finales del os noventa, conozco morphic en el mundo Smalltalk (gracias a las reuniones de SUGAR Smalltalk User Group Argentina, ahora disuelto). Ver History of Morphic. Y Naked Objects es otra forma de exponer el modelo al usuario/desarrolladore. Ver Naked Objects, Wikipedia. Leo ahí:
1. All business logic should be encapsulated onto the domain objects. This principle is not unique to naked objects: it is just a strong commitment to encapsulation.
2. The user interface should be a direct representation of the domain objects, with all user actions consisting, explicitly, of creating or retrieving domain objects and/or invoking methods on those objects. This principle is also not unique to naked objects: it is just a specific interpretation of an object-oriented user interface (OOUI).
The original idea in the naked objects pattern arises from the combination of these two, to form the third principle:
3. The user interface should be created 100% automatically from the definition of the domain objects. This may be done using several different technologies, including source code generation; implementations of the naked objects pattern to date have favoured the technology of reflection.
The naked objects pattern was first described formally in Richard Pawson's PhD thesis[1] which includes a thorough investigation of various antecedents and inspirations for the pattern including, for example, the Morphic user interface.
Y en este siglo, también encontré Magritte.
Ok, es tiempo de explorar la forma runtime/reflection/lambdas en .NET. Pros: los desarrolladores pueden extender el modelo desde la IDE y escribiendo código. Cons: está limitado a una tecnología, en este caso .NET; nuevos modelos dinámicos de interfaz pueden ser difíciles de escribir; soportar extensiones “custom” o manuales puede no ser fácil.
Bien, suficiente por ahora, vuelvo a codificar (y al trabajo). Sigo teniendo el Efecto Coto ;-)
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
El code kata de este pasado fin de semana fue algo que estaba pensando desde el último año: ejecutar AjSharp (mis post en español de AjSharp) dentro de un Worker Role de Azure. Pero desde antes que pienso: un intérprete como AjSharp puede servir de lenguaje de scripting en sistemas distribuidos. Esta incursión en Azure es una prueba de concepto interesante.
La idea es: una instancia de worker role recibe un texto via una cola de mensaje. El texto contiene código AjSharp y lo ejecuta. La salida (digamos, los PrintLine) se captura como texto y se envía como mensaje a otra cola.
El resultado del ejercicio está dentro de mi AjCodeKata project: tienen que bajarse trunk\Azure\AzureAjSharp Y TAMBIEN trunk\AjLanguage (donde está el proyecto AjSharp).
La solución:
Los proyectos:
AzureAjSharp.WorkerRole: un worker role de ejemplo, con estas líneas agregadas:
CloudStorageAccount account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
Processor processor = new Processor(account);
processor.Start();
Azure.AjSharp: la librería de clases principal del proyecto. Contiene una clase Processor. Los constructores necesitan una cuenta de cloud y los nombres (opcionales, hay valores “default”): cola de requests, responses y un blob container. La cola de request contiene mensajes con código AjSharp para ejecutar (recuerden que AjSharp puede acceder a clases y objetos .NET: pueden invocar código compilado tranquilamente). La cola de response tiene el texto de salida de esas ejecuciones. El código de arriba processor.Start() inicia la lectura y ejecución de esos mensajes AjSharp.
AzureAjSharp.Console: Es el programa que lee líneas de la consola, y cuando ingresamos una línea que contiene sólo “send”, el texto ingresado se convierte en un mensaje Azure que va a la cola de requests. Este programa también va leyendo (en un Thread aparte) los mensajes que le llegan por la cola de responses, imprimiendo el resultado.
AzureLibrary: Clases auxiliares que usé en otros ejemplos de Azure.
AjSharpVS2010, AjLanguageVS2010: Implementación de AjSharp .
Cuando ejecuto el programa de consola, puedo enviar código AjSharp para ejecutar en un worker role, y recibir el resultado:
Vean que uso clases .NET como DirectoryInfo.
Y hay más: AjSharp soporta el Include(“nombredearchivoaserincluido”); donde el archivo contiene más código AjSharp. Modifiqué el lanzamiento de la máquina AjSharp para que tenga una versión cambiada de Include: ahora busca el contenido en un blob. Eso permite incluir y reusar código que no quepa en un mensaje, desde un blob container
Un gráfico:
Entonces, subí algo de código (los archivos originales están en el directorio Examples del proyecto Azure.AjSharp) en el contenedor blob llamado ajsfiles (para esta prueba en mi DevStorage):
(Estoy usando Neudesic Azure Storage Explorer, pero podría usar también CloudBerry Explorer for Azure Storage: soporta el manejo de carpetas en árbol).
Esta es el resultado de la ejecución de código con include de HelloWorld.ajs, y ForOneToTen.ajs:
Próximos pasos:
- Escribir más código utilitario en AjSharp, para ser incluido en otros programas, como: utilitarios de manejo de File y Directory, download y upload de blobs, envío y recepción de mensajes en cola, mensajes a todos los worker role intances, bajada y carga de assemblies, objetos a compartir entre request y request de AjSharp, etc. El cielo es el límite! ;-)
Entonces, podemos (nosotros o un programa) enviar tareas dinámicamente y recibir resultados. Algo para tener: Guids para identificar tareas y sus resultados; una interfaz web; resultados almacenados como blobs; cache (y flush) de los archivos incluidos, etc…
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Hoy, temprano, encuentro en mi “timeline” a gente de España. En estos días se está celebrando el SolidQ Summit 2011 en Madrid:
http://summit.solidq.com/madrid/2011/Paginas/Home.aspx
Un video:
Pueden leer el post de @erincon:
Cinco razones por las que debes asistir al SQLU Summit 2011
También encontré tweets del bueno de Daniel Seara, les dejo acá los consejos/mandamientos sobre SQL Server (supongo que se pueden aplicar a otras bases de datos):
Yo, como desarrollador, siempre estoy en alguna falta con las bases de datos: un tema que varios usamos de oído. Habrá que ponerse las pilas y estudiar! ;-)
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
En este siglo escribí muchos posts sobre generación de código. Es un tema que me interesa desde los ochenta del siglo pasado: cómo hacer programas que escriban programas. Pero no es solamente generación de código: quiero poner énfasis en generación de código DESDE UN MODELO. Ahora, quiero escribir sobre la base de esa solución pragmática a algunos de los problemas que encontramos en el desarrollo de software. Mi principal afirmación:
Generación de Código desde un Modelo es un ejercicio de Elevar el Nivel de Abstracción
La historia de la programación es una historia de “elevar el nivel de abstracción”. En lugar de manipular botones en el panel frontal de las primeras computadoras, ahora escribimos programas usando un lenguaje que es una abstracción: usamos “int a” como una abstracción de una palabra en memoria. Escribimos “foo(bar)” como abstracción de guardar el estado en la pila, y saltar a una dirección en memoria: ya no escribimos más instrucciones BALR (Branch and Link Register) en un lenguaje ensamblador de mainframe IBM. Entonces, cada día estamos usando generación de código: desde el lenguaje general o específico que usamos para escribir programas, nuestros compiladores generan código de máquina, el único código que cuenta para las computadoras.
En esta nueva serie de post quiero explorar y escribir sobre estas ideas:
- Nosotros mejoramos continuamente nuestras ideas y habilidades, pero la tecnología también está sujeta a cambio constante
- Modelos abstractos vs modelos tecnológicos, concretos
- Separar los problemas de las soluciones
- Destilar y reusar el conocimiento de los desarrolladores
- Generación de Código DESDE UN MODELO como un ejercicio de abstracción
- Entonces, la generación de código desde un modelo tiene un efecto beneficioso colateral: arroja luz sobre la separación entre lo que es importante y lo que es accidental o técnico
- La generación de código puede (y debe) coexistir con la codificación manual
Muchos de estos temas fueron mencionados en mis posts de AjGenesis, pero es tiempo de presentarlos de nuevo como un todo integrado. El título de esta serie “Raise the Level of Abstraction” (ver mi post en inglés) nació en una charla técnica en uno de mis clientes, ya hace unos años. Es bueno encontrar la misma frase como “leit motiv” para la conferencia Code Generation 2011:
http://www.codegeneration.net/cg2011/index.php
Recuerdo también otra frase anterior de ese sitio: “code generation for the pragmatic engineer”. La generación de código desde un modelo no es una “bala de plata”: es otra bala, herramienta más, a usar en el contexto correcto.
(Nota: Encontré las imágenes que acompañan a este post buscando el Google Images, con términos: “two apples”, “two oranges”, “two sheep” and….. “two person” ;-)… no busqué Microsoft ;-))
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez
Anterior Post
Siguiente Post
En el anterior post, implementé la recuperación de los datos de un Tema de libros. No implementé aún ninguna vista. Estoy escribiendo los tests, e implementando el código del controlador. Es hora de agregar un nuevo Tema (Subject), usando una acción del controlador.
Este fue mi primer test:
[TestMethod]
public void AddSubject()
{
IList<Subject> subjects = GetSubjects();
Subject subject = new Subject() { Name = "Chemistry" };
SubjectController controller = new SubjectController(subjects);
ActionResult result = controller.Create(subject);
Assert.IsNotNull(result);
Assert.IsTrue(subjects.Any(s => s.Name == "Chemistry"));
Assert.AreNotEqual(0, subject.Id);
}
GetSubjects() es un método ayudante que presenté en el anterior posts. Retorna una lista de temas para tests. Agregué una nueva acción en la clase controladora, para que la solución compile:
public ActionResult Create(Subject subject)
{
throw new NotImplementedException();
}
Compiló, y el test quedó en rojo:
Entonces, completé el método de acción con el código mínimo para que pase el test:
public ActionResult Create(Subject subject)
{
subject.Id = this.subjects.Max(s => s.Id);
subjects.Add(subject);
return RedirectToAction("Index");
}
Pero quiero redireccionar a la vista de detalle luego de haber dado de alta un item. Agrego el test:
RedirectToRouteResult redirect = (RedirectToRouteResult)result;
Assert.IsTrue(string.IsNullOrEmpty(redirect.RouteName));
Assert.IsTrue(redirect.RouteValues.ContainsKey("id"));
Assert.AreEqual(subject.Id, redirect.RouteValues["id"]);
Assert.IsTrue(redirect.RouteValues.ContainsKey("action"));
Assert.AreEqual("Details", redirect.RouteValues["action"]);
Cambio el código de la acción para que cumpla con el nuevo requerimiento:
public ActionResult Create(Subject subject)
{
subject.Id = this.subjects.Max(s => s.Id);
subjects.Add(subject);
return RedirectToAction("Details", new { id = subject.Id });
}
Ahora, el test en verde:
Seguí un camino similar para escribir el test de actualización de un Tema existente. Esta es la versión actual de ese test:
[TestMethod]
public void UpdateSubject()
{
IList<Subject> subjects = GetSubjects();
Subject literature = subjects.Where(s => s.Name == "Literature").FirstOrDefault();
Subject subject = new Subject() { Name = "SciFi" };
SubjectController controller = new SubjectController(subjects);
ActionResult result = controller.Update(literature.Id, subject);
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
RedirectToRouteResult redirect = (RedirectToRouteResult)result;
Assert.IsTrue(string.IsNullOrEmpty(redirect.RouteName));
Assert.IsTrue(redirect.RouteValues.ContainsKey("id"));
Assert.AreEqual(literature.Id, redirect.RouteValues["id"]);
Assert.IsTrue(redirect.RouteValues.ContainsKey("action"));
Assert.AreEqual("Details", redirect.RouteValues["action"]);
Assert.IsTrue(subjects.Any(s => s.Name == "SciFi"));
Assert.AreEqual(literature.Id, subjects.Where(s => s.Name == "SciFi").Single().Id);
}
La acción del controlador:
public ActionResult Update(int id, Subject subject)
{
Subject toupdate = this.subjects.Where(s => s.Id == id).Single();
toupdate.Name = subject.Name;
return RedirectToAction("Details", new { id = id });
}
Todos los tests en verde:
Algunas advertencias: los tests están revisando dos salidas: los cambios en el dominio (por ahor auna simple lista) y los resultados de las acciones (un tema MVC). Debería hacer refactor (seguramente en un futuro post) de la implementación del controlador, para separar las actualizaciones del dominio de los resultados de la navegación. Escribí el ejemplo de esta manera para mostrar cómo hacer refactor más adelante para conseguir una mejor implementación. Mi idea es tener un servicio de manejo de temas, y escribir con tests. Luego usarlo desde el controlador. Debería tambien tratar los casos de actualización de un item inexistente o la creación de un item ya existente.
Otra nota: no puse ninguna vista MVC todavía. Pero ahora ya tengo los tests como para agregarlas con confianza. Cuando las agregue, la conducta esperada del controlador ya estará probada.
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez