viernes, 19 de junio de 2015

JqGrid en Aplicaciones MVC con Entity Framework

En muchas aplicaciones web es necesario usar grillas que muestren la información contenida en una base de datos y permitan ordenar, filtrar y paginar los datos. Dependiendo de las herramientas que se usen, esto puede requerir más o menos trabajo. Este artículo presenta una combinación de herramientas para facilitar el manejo de grillas JqGrid de forma sencilla y con buena performance, debido a que el filtrado, ordenamiento y paginado se hace a nivel de base de datos.

Para el ejemplo utilizaremos Entity Framework, pero se puede adaptar fácilmente a NHibernate.

El código de ejemplo está creado con Visual Studio 2013.

 

Creación del proyecto

1- Crear un nuevo proyecto MVC en Visual Studio

clip_image002

2- Seleccionar un proyecto de tipo MVC y configurar la autenticación del proyecto como “No Authentication” (para hacerlo lo más simple posible)

clip_image004

3- Agregar los siguientes paquetes Nuget, en su última versión:

· Entity Framework

clip_image005

· JqGrid

clip_image006

· MvcJqGrid

clip_image007

4- En el Nuget Package Manager, además actualizar todos los paquetes con la opción “Update All”, de manera de trabajar con las últimas versiones.

 

Grilla básica

La aplicación que vamos a crear es una aplicación MVC básica. En una aplicación real sería conveniente crear capas intermedias para las diferentes responsabilidades, pero en este caso vamos a interactuar con el repositorio de datos directamente desde el Controller para simplificar el ejemplo.

1- Creamos dos clases de modelo en la carpeta Models:

Employee

public class Employee

{

[Key]

public virtual int Id { get; set; }

public virtual string FirstName { get; set; }

public virtual string LastName { get; set; }

public virtual decimal Salary { get; set; }

public virtual Organization Organization { get; set; }

}

Organization

public class Organization

{

[Key]

public virtual int Id { get; set; }

public virtual string Name { get; set; }

public virtual string Address { get; set; }

}

2- Creamos un nuevo Controller en la carpeta controllers, con el template de Entity Framework

clip_image008

3- Seleccionamos el modelo y creamos un nuevo context

clip_image009

4- Agregamos un link al nuevo controller en Views/Shared/_Layout.cshtml

<li>@Html.ActionLink("Employees", "Index", "Employee")</li>

Si en este momento ejecutamos la aplicación y vamos a la ruta /Employee, aparece el listado de empleados

5- En este punto deberíamos poder crear algunos empleados de ejemplo

clip_image011

Si abrimos la base de datos desde Visual Studio deberíamos poder ver las tablas que creó Entity Framework.

clip_image012

6- Agregamos algunos datos de prueba en las dos tablas (al menos 6 empleados, así se puede probar la paginación):

clip_image013

clip_image014

Ahora vamos a reemplazar el listado por default que creó el proyecto de MVC por una grilla de JqGrid

7- Borramos la tabla y la cambiamos por este código usando el helper de MvcJqGrid para crear una grilla. También se podría agregar directamente la grilla por Javascript:

@(Html.Grid("employee")

.SetCaption("Employees")

.AddColumn(new Column("FirstName").SetLabel("First Name"))

.AddColumn(new Column("LastName").SetLabel("Last Name"))

.AddColumn(new Column("Salary").SetLabel("Salary").SetSearchOption(SearchOptions.Equal))

.AddColumn(new Column("Organization.Name").SetLabel("Org. Name"))

.AddColumn(new Column("Organization.Address").SetLabel("Org. Address"))

.SetUrl("/Employee/List/")

.SetAutoWidth(true)

.SetHeaderTitles(true)

.SetViewRecords(true)

.SetPager("pager")

.SetSearchToolbar(true)

.SetSearchOnEnter(false)

.SetSearchClearButton(true)

.SetSearchToggleButton(false)

.SetSortName("LastName")

.SetRowNum(5))

8- En el controller EmployeeController agregamos el método List, en donde obtenemos los empleados de la base de datos y los transformamos a un objeto JSON en el formato que espera JqGrid:

public ActionResult List()

{

var employees = db.Employees.ToList();

var jsonData = new

{

page = 1,

records = employees.Count,

rows = (

from e in employees

select new

{

id = e.Id,

cell = new Object[]

{

e.FirstName,

e.LastName,

e.Salary

}

}).ToArray()

};

return Json(jsonData, JsonRequestBehavior.AllowGet);

}

El paquete NuGet de JqGrid no agrega automáticamente bundles para Javascript y CSS, por lo cual hay que agregarlos manualmente.

9- En App_Start/BundleConfig.cs agregamos bundles para JqGrid

bundles.Add(new ScriptBundle("~/bundles/jqgrid").Include(

"~/Scripts/i18n/grid.locale-en.js",

"~/Scripts/jquery.jqGrid.min.js"));

bundles.Add(new StyleBundle("~/Content/cssJqGrid").Include(

"~/Content/jquery.jqGrid/ui.jqgrid.css",

"~/Content/themes/base/all.css"));

10- Y los agregamos en el layout, en la sección <head>en este orden (es necesario mover el bundle de JQuery agregado al crear el proyecto a la sección de <head>)

@Styles.Render("~/Content/css")

@Styles.Render("~/Content/cssJqGrid")

@Scripts.Render("~/bundles/modernizr")

@Scripts.Render("~/bundles/jquery")

@Scripts.Render("~/bundles/jqgrid")

Si ejecutamos la aplicación en este momento ya se deberían ver los datos de empleados en la grilla de JqGrid. En este punto todavía no va a funcionar el ordenamiento, filtros ni paginado.

clip_image016

Agregar acciones en la grilla

Para agregar las funcionalidades de ordenamiento, filtros y paginado tenemos que modificar el método List de EmployeeController para que tome como parámetro un objeto GridSettings y agregar unas clases de soporte.

1- Agregamos las clases PagedQuery, PagedList y JqGridExtensions. Al final del artículo se muestra el código de estas clases, que se encargan de transformar los parámetros de JqGrid en queries que Entity Framework pueda ejecutar.

2- Modificamos el método List del Controller:

public ActionResult List(GridSettings grid)

{

var query = grid.ToPagedQuery<Employee>();

var employees = query.ExecuteOn(db.Employees);

var jsonData = new

{

total = (int)Math.Ceiling((double)employees.TotalItems / employees.PageSize),

page = employees.PageNumber,

records = employees.TotalItems,

rows = (

from e in employees

select new

{

id = e.Id,

cell = new Object[]

{

e.FirstName,

e.LastName,

e.Salary

}

}).ToArray()

};

return Json(jsonData, JsonRequestBehavior.AllowGet);

}

Con estos cambios ya deberían funcionar automáticamente el ordenamiento, paginado y los filtros, todo ejecutado directamente en la base de datos. En la siguiente imagen se puede ver el filtrado por la segunda columna:

clip_image018

Entidades relacionadas

El mismo esquema funciona con entidades relacionadas, simplemente usando la notación EntidadRelacionada.Propiedad

1- Agregamos dos columnas correspondientes a la entidad Organization en la misma grilla, modificando el método List del controller:

AddColumn(new Column("Salary").SetLabel("Salary").SetSearchOption(SearchOptions.Equal))

.AddColumn(new Column("Organization.Name").SetLabel("Org. Name"))

.AddColumn(new Column("Organization.Address").SetLabel("Org. Address"))

.SetUrl("/Employee/List/")

2- Y las agregamos también en el método List del controller:

e.Salary,

e.Organization.Name,

e.Organization.Address

}

Con estos cambios alcanza para que se vean las columnas relacionadas en la grilla y funcionen los filtros y el ordenamiento a través de la relación Employee – Organization.

clip_image020

Clases auxiliares

PagedQuery

Esta clase mantiene y aplica los criterios de filtros, ordenamiento y paginado. Trabaja sobre IQueryable en forma genérica, por lo que se puede aplicar a cualquier fuente de datos. En este caso la usamos contra un repositorio de EntityFramework.

public class PagedQuery<TModel>

{

public Expression<Func<TModel, bool>> Condition { get; set; }

private Expression<Func<TModel, object>>[] SortKeySelectors { get; set; }

private int PageSize { get; set; }

private int Page { get; set; }

private bool[] SortAscending { get; set; }

private PagedQuery(Expression<Func<TModel, bool>> condition)

{

PageSize = 0;

Page = 0;

Condition = condition;

SortKeySelectors = new Expression<Func<TModel, object>>[0];

SortAscending = new bool[0];

}

public static PagedQuery<TModel> All()

{

return new PagedQuery<TModel>(null);

}

public PagedQuery<TModel> FetchPage(int page)

{

Page = page;

return this;

}

public PagedQuery<TModel> Size(int pageSize)

{

PageSize = pageSize;

return this;

}

private PagedQuery<TModel> OrderBy(Expression<Func<TModel, object>>[] sortKeySelectors, bool[] ascending)

{

SortKeySelectors = sortKeySelectors;

SortAscending = ascending;

return this;

}

public PagedQuery<TModel> OrderBy(IEnumerable<string> propertyNames, bool[] ascending)

{

return OrderBy(propertyNames.Select(NestedPropertyGet).ToArray(), ascending);

}

public static IOrderedQueryable<TModel> ObjectSort(IQueryable<TModel> entities, Expression<Func<TModel, object>> expression, bool ascending)

{

var unaryExpression = expression.Body as UnaryExpression;

if (unaryExpression != null)

{

var propertyExpression = (MemberExpression)unaryExpression.Operand;

var parameters = expression.Parameters;

if (propertyExpression.Type == typeof(DateTime))

{

var newExpression = Expression.Lambda<Func<TModel, DateTime>>(propertyExpression, parameters);

return ascending ? entities.OrderBy(newExpression) : entities.OrderByDescending(newExpression);

}

if (propertyExpression.Type == typeof(int))

{

var newExpression = Expression.Lambda<Func<TModel, int>>(propertyExpression, parameters);

return ascending ? entities.OrderBy(newExpression) : entities.OrderByDescending(newExpression);

}

if (propertyExpression.Type == typeof(decimal))

{

var newExpression = Expression.Lambda<Func<TModel, decimal>>(propertyExpression, parameters);

return ascending ? entities.OrderBy(newExpression) : entities.OrderByDescending(newExpression);

}

throw new NotSupportedException("Object type resolution not implemented for this type");

}

return ascending ? entities.OrderBy(expression) : entities.OrderByDescending(expression);

}

public PagedList<TModel> ExecuteOn(IQueryable<TModel> repository)

{

int totalItems = 0;

repository = ApplyFilter(repository);

if (PageSize > 0)

{

totalItems = repository.Count();

}

repository = ApplySorting(repository);

repository = ApplyPaging(repository);

return new PagedList<TModel>(repository.ToList(), Page, PageSize, totalItems);

}

private IQueryable<TModel> ApplyFilter(IQueryable<TModel> repository)

{

if(Condition != null)

{

repository = repository.Where(Condition);

}

return repository;

}

private IQueryable<TModel> ApplySorting(IQueryable<TModel> repository)

{

for (int i = 0; i < SortKeySelectors.Length; i++)

{

repository = ObjectSort(repository, SortKeySelectors[i], SortAscending[i]);

}

return repository;

}

private IQueryable<TModel> ApplyPaging(IQueryable<TModel> repository)

{

if(PageSize > 0)

{

repository = repository.Skip((Page - 1)*PageSize).Take(PageSize);

}

return repository;

}

private static Expression<Func<TModel, object>> NestedPropertyGet(string propertyChain)

{

var properties = propertyChain.Split('.');

var type = typeof(TModel);

var parameter = Expression.Parameter(type, "x");

Expression expression = parameter;

PropertyInfo propertyInfo = null;

foreach (var propertyName in properties)

{

propertyInfo = type.GetProperty(propertyName);

expression = Expression.Property(expression, propertyInfo);

type = propertyInfo.PropertyType;

}

if (propertyInfo != null && propertyInfo.PropertyType.IsValueType)

{

expression = Expression.Convert(expression, typeof(object));

}

return Expression.Lambda<Func<TModel, object>>(expression, parameter);

}

}

PagedList

Es una clase simple, sin lógica, que contiene los resultados de una consulta paginada.

public class PagedList<TModel> : IEnumerable<TModel>

{

public int PageNumber { get; private set; }

public int PageSize { get; private set; }

public int TotalItems { get; private set; }

private IList<TModel> Items { get; set; }

public PagedList(IList<TModel> items, int pageNumber, int pageSize, int totalItems)

{

Items = items;

PageNumber = pageNumber;

PageSize = pageSize;

TotalItems = pageSize == 0 ? items.Count : totalItems;

}

public IEnumerator<TModel> GetEnumerator()

{

return Items.GetEnumerator();

}

IEnumerator IEnumerable.GetEnumerator()

{

return GetEnumerator();

}

}

JqGridExtensions

Se encarga del mapeo entre los parámetros de JqGrid y la clase PagedQuery. Contiene el método ToPagedQuery, que crea un PagedQuery en base a los parámetros enviados por JqGrid, conteniendo los criterios de ordenamiento y filtrado, así como los datos relacionados con el paginado.

public static class JqGridExtensions

{

public static PagedQuery<TModel> ToPagedQuery<TModel>(this GridSettings settings)

{

var query = PagedQuery<TModel>.All().Size(settings.PageSize).FetchPage(settings.PageIndex);

if (settings.Where != null)

{

foreach (var rule in settings.Where.rules)

{

query.Condition = query.Condition.Where(rule.field, rule.data, rule.op);

}

}

var sortColumns = (settings.SortColumn + " " + settings.SortOrder).Split(',');

if (!String.IsNullOrWhiteSpace(settings.SortColumn) && sortColumns.Length > 0)

{

var propertyNames =

sortColumns.Select(x => x.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries)[0].Trim())

.ToArray();

var ascending =

sortColumns.Select(

x => x.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries)[1].Trim() == "asc")

.ToArray();

query = query.OrderBy(propertyNames, ascending);

}

return query;

}

private static Expression<Func<T, bool>> Where<T>(this Expression<Func<T, bool>> query,

string column, object value, string operation)

{

if (string.IsNullOrEmpty(column))

return query;

ParameterExpression parameter = Expression.Parameter(typeof (T), "p");

MemberExpression memberAccess = null;

Expression<Func<T, bool>> finalLambda = null;

foreach (var property in column.Split('.'))

{

memberAccess = Expression.Property

(memberAccess ?? (parameter as Expression), property);

}

if (value == null)

{

return query;

}

foreach (var val in value.ToString().Split(new[] {','}))

{

Expression<Func<T, bool>> lambda = null;

Expression condition = GetCondition(val, memberAccess, operation);

if (condition != null)

lambda = Expression.Lambda<Func<T, bool>>(condition, parameter);

finalLambda = finalLambda == null ? lambda : CombineOr(finalLambda, lambda);

//Si es string vacío también busca por NULL

if (val == string.Empty)

{

condition = GetCondition(null, memberAccess, operation);

if (condition != null)

lambda = Expression.Lambda<Func<T, bool>>(condition, parameter);

finalLambda = finalLambda == null ? lambda : CombineOr(finalLambda, lambda);

}

}

return query == null ? finalLambda : Combine(query, finalLambda);

}

private static Expression GetCondition(string val, MemberExpression memberAccess, string operation)

{

var filter = Expression.Constant

(

ChangeType(val, memberAccess.Type)

);

Expression condition = null;

switch (operation)

{

//equal ==

case "eq":

condition = Expression.Equal(memberAccess, Expression.Convert(filter, memberAccess.Type));

break;

//not equal !=

case "ne":

condition = Expression.NotEqual(memberAccess, filter);

break;

//begins with !=

case "bw":

condition = Expression.Call(memberAccess,

typeof(string).GetMethod("StartsWith", new[] { typeof(string) }),

Expression.Constant(val));

break;

//string.Contains()

case "cn":

condition = Expression.Call(memberAccess,

typeof(string).GetMethod("Contains"),

Expression.Constant(val));

break;

//not begins with !=

case "ew":

condition = Expression.Call(memberAccess,

typeof(string).GetMethod("EndsWith", new[] { typeof(string) }),

Expression.Constant(val));

break;

//less than <

case "lt":

condition = Expression.LessThan(memberAccess, filter);

break;

//less or equal <=

case "le":

condition = Expression.LessThanOrEqual(memberAccess, filter);

break;

//greater than >

case "gt":

condition = Expression.GreaterThan(memberAccess, filter);

break;

//greater or equal than >=

case "ge":

condition = Expression.GreaterThanOrEqual(memberAccess, filter);

break;

}

return condition;

}

private static Expression<Func<T, bool>> Combine<T>(Expression<Func<T, bool>> filter1, Expression<Func<T, bool>> filter2)

{

var rewrittenBody1 = new ReplaceVisitor(

filter1.Parameters[0], filter2.Parameters[0]).Visit(filter1.Body);

var newFilter = Expression.Lambda<Func<T, bool>>(

Expression.AndAlso(rewrittenBody1, filter2.Body), filter2.Parameters);

return newFilter;

}

private static Expression<Func<T, bool>> CombineOr<T>(Expression<Func<T, bool>> filter1, Expression<Func<T, bool>> filter2)

{

var rewrittenBody1 = new ReplaceVisitor(

filter1.Parameters[0], filter2.Parameters[0]).Visit(filter1.Body);

var newFilter = Expression.Lambda<Func<T, bool>>(

Expression.OrElse(rewrittenBody1, filter2.Body), filter2.Parameters);

return newFilter;

}

class ReplaceVisitor : ExpressionVisitor

{

private readonly Expression from, to;

public ReplaceVisitor(Expression from, Expression to)

{

this.from = from;

this.to = to;

}

public override Expression Visit(Expression node)

{

return node == from ? to : base.Visit(node);

}

}

private static object ChangeType(string value, Type conversionType)

{

if (conversionType == null)

{

throw new ArgumentNullException("conversionType");

}

if (conversionType.IsEnum)

{

return Enum.Parse(conversionType, value);

}

Guid result;

if (Guid.TryParse(value,out result))

{

return Guid.Parse(value);

}

if (conversionType.IsGenericType &&

conversionType.GetGenericTypeDefinition() == typeof(Nullable<>))

{

if (value == null)

{

return null;

}

var nullableConverter = new NullableConverter(conversionType);

conversionType = nullableConverter.UnderlyingType;

}

return Convert.ChangeType(value, conversionType);

}

}

 

Código completo

En el archivo zip encontrarán la solución completa del ejemplo en Visual Studio 2013.


¡Gracias Guillermo Vasconcelos por tu contribución!

miércoles, 10 de junio de 2015

¿Un Tester QA debe programar?

clip_image002

El Tester de hoy, por naturaleza, debe ser un profesional con elevada capacidad de comunicación, proactivo, analítico, metódico y con conocimientos técnicos que le permitan la ejecución exitosa del ciclo de vida de prueba del software.

Actualmente, las empresas tienen una mayor demanda del servicio Testing QA y eso ha sido una parte fundamental en la reformulación del perfil que debe tener un Tester.

Este crecimiento en la demanda y el hecho de que hoy por hoy el desarrollo de las aplicaciones se han vuelto más complejas y requieren mayor integración con otros sistemas; hicieron surgir la necesidad de automatizar las pruebas, para disminuir los tiempos empleados en el STLC y lograr mayores niveles de calidad.

La implementación de herramientas de automatización para hacer una gestión más efectiva en el tiempo de prueba ha generado que el Tester deba adquirir conocimientos de programación.

La programación, que siendo una disciplina de la Ingeniería de Software, nos va a permitir aprovechar todo el potencial de nuestra profesión.

Qué beneficios podemos obtener como Tester:

· Mejoras en la organización de las pruebas.

· Mayor capacidad para aplicar pruebas complicadas.

· Entendimiento de los problemas que pueda tener la aplicación en las capas anteriores del frontend y a nivel técnico precisar el bug detectado.

· Mejoras en la comunicación con el equipo, ya que al brindar un sistema para recolectar y diseminar información de manera eficaz, proporcionamos una retroalimentación oportuna al equipo de programación.

· Diseño e implementación de script’s para la automatización de pruebas funcionales.

· Estabilización temprana del código evitando el re-trabajo.

· Habilitación de pruebas de regresión y disminución del tiempo aplicado en los ciclos de pruebas.

· Realización de un mayor número de pruebas. Algunos de los problemas hallados por la automatización, tal vez no hubieran sido encontrados utilizando sólo pruebas manuales, debido a limitantes de tiempo.

· Mayor confiabilidad en los resultados.

· Esto nos facilita analizar cómo nuestra labor complementa y asiste a otras áreas. Somos más valiosos como parte integral de la empresa que como un elemento aislado de la misma.

Como les comenté anteriormente, existe un incremento en la demanda de las áreas de Testing de las empresas. Ahora bien, por un motivo de costos, la búsqueda tiene sus exigencias. El Tester buscado es aquel que tenga conocimientos de programación en herramientas de automatización para las pruebas de regresión. Y sumado a los distintos tipos de pruebas (Sistema, integradas, funcional, no funcional), lo harán un recurso muy valioso.

Este punto me invita a pensar. ¿ El Tester Funcional tiene sus días contados?

clip_image004

Pienso que NO. Conozco a muchos Testers que tienen conocimientos del funcionamiento de una aplicación y sus distintas capas, y junto a los conocimientos del negocio le permiten hacer un test exploratorio o manual más eficiente que un programador.

El Tester debe combinar sus conocimientos para poder aumentar su efectividad de ejecución de los ciclos de pruebas y continuar con el crecimiento profesional, a la par del avance tecnológico que le permitirá ser un recurso más preciado.

Siendo una necesidad el aprendizaje de un lenguaje de programación de acuerdo a la meta del Tester o al objetivo del proyecto. ¿Cuál es el más conveniente (Java, .net, php, ruby, Javascritp python, csharp)?

Si bien el tester funcional seguirá activo en los proyectos, cada vez toman más fuerza los testers que manejan distintas herramientas y que conocen lenguajes de programación capaces de automatizar tareas.

clip_image006

 

 

 

 

Autor:

Álvaro Guaramato

Líder Técnico de QA asignado al proyecto del Assessment de Testing & QA en Banco Hipotecario.

viernes, 5 de junio de 2015

Trunks, tags & branches

¡Hola! Les acerco mi experiencia en la organización más eficiente de un directorio de control de versiones de código fuente; especialmente en los casos en donde tenemos diferentes desarrollos en paralelo, o tenemos que realizar diversos releases intermedios, y mantenerlos, mientras seguimos desarrollando una nueva funcionalidad aparte.

Si bien mi experiencia se centra en TFS y SVN, con los cuales vengo trabajando ya desde hace ya varios años, estos conceptos pueden aplicarse también a otros sistemas como GIT, CVS, Mercurial, etc.

Introducción a las herramientas de SCM

Sin ahondar mucho en hechos históricos, podemos inferir que cuando surgieron los equipos de desarrollo de software, nacieron los problemas de tener que sincronizar el trabajo que cada uno de los miembros realizaba. Integrar los cambios y modificaciones, volver atrás una versión, trackear un cambio para detectar cuando se hizo (o quien lo hizo), entre otras cosas, se volvieron actividades comunes en los diferentes proyectos que se realizaban.

Y así surgieron las primeras herramientas de repositorio, que nos brindaron la posibilidad de almacenar nuestro código fuente, versionándolo, resguardándolo y agregándole la metadata necesaria para poder realizar el tracking correspondiente. Con el paso del tiempo fueron evolucionando, convirtiéndose en las hoy llamadas herramientas de SCM (por Software Configuration Management) con una mayor cantidad de características y beneficios, ya que no sólo se encargan de administrar los elementos de nuestro software, sino que además ofrecen soporte a la gestión y administración de los proyectos.

¿Qué beneficios nos aportan las herramientas de SCM?

Actualmente es casi imposible pensar en llevar adelante un proyecto con éxito sin contar con una de estas herramientas para el equipo. Y aún en el caso de que la solución fuera desarrollada por una única persona, el contar con una herramienta de SCM trae numerosos beneficios, más allá de la sincronización de nuestro código fuente:

· Mantenimiento de la integridad de los componentes de nuestra solución (sea código fuente o no)

· Evaluación y tracking de cambios

· Auditoría de la configuración

· Respaldo de versiones anteriores de cada uno de los elementos

· Gestión de actividades y tareas

· Planificación y seguimiento del estado de avance

· Trazabilidad horizontal (tareas asignadas vs. changesets de código)

· Reducción de costos del proyecto como producto de mantener todo más ordenado

Definiciones de Trunk, Branch y Tag

Ahora bien, pasemos a definir tres de los conceptos más importantes que deberemos manejar a la hora de trabajar con una herramienta de SCM y sobre los cuales luego vamos a discutir diferentes estrategias de organización y algunas buenas prácticas de trabajo para que apliquemos en nuestros proyectos.

clip_image001

Trunk (Tronco): Es la línea principal de desarrollo, en donde se realizan los cambios menos importantes o con un menor impacto en el día a día del proyecto. Además, es el lugar en donde se integran los cambios realizados en otros branches una vez que están finalizados y validados. Suele haber un único trunk por proyecto.

Branch (Rama): Es una copia del código fuente, derivada del trunk u otro branch, en un determinado instante. Permite trabajar en paralelo sobre sí misma sin afectar el trunk o el branch de origen. Básicamente existen de dos tipos: los que se utilizan para introducir cambios, correcciones o mejoras significativas en nuestra aplicación, y los que se generan como producto del lanzamiento de un nuevo release.

Tag (Etiqueta): Permite identificar un cierto momento en nuestro ciclo de desarrollo que deseamos preservar para luego poder reconstruir nuestra aplicación tal cual estaba en dicho instante. En otras palabras, un tag es un snapshot. Generalmente se los crea para identificar y preservar un nuevo release o un punto de estabilidad ante futuros cambios.

Otro concepto muy usado actualmente en la gestión de nuestros repositorios de código fuente, especialmente en herramientas más modernas, es el concepto de fork (bifurcación), que es cuando se crea un nuevo proyecto a partir de otro replicando el código fuente en dicho instante, pero con una dirección distinta a la original. En los casos de los proyectos open source se suele dar con mayor frecuencia, especialmente cuando surgen diferencias entre los desarrolladores, o cuando se quiere partir del producto actual para realizar una versión independiente (como por ejemplo sucede con las distribuciones de Linux). En el ámbito de los proyectos de software propietario se suelen dar con menor frecuencia, por ejemplo, cuando se quiere llevar una versión de una aplicación a otro país y hay que aplicar cambios importantes, pero independientes a los del producto original.

Buenas prácticas en la organización del trunk, los branches y los tags

En lo que respecta a la interacción que los desarrolladores y arquitectos tenemos con las herramientas de SCM, quiero enfocarme en esta parte del artículo en algunas buenas prácticas y estrategias acerca de cómo podríamos organizar mejor nuestro repositorio para responder satisfactoriamente al desarrollo de actividades en paralelo, o tener disponibles múltiples versiones de un sistema en el mismo instante. Por ejemplo, para que puedan mantenerse y evolucionarse mientras dichas versiones que viven en los diferentes Stages Pre-productivos de nuestros clientes.

En lo que respecta al manejo del trunk, les propongo las siguientes buenas prácticas para aplicar en sus proyectos de desarrollo:

· Cada uno de nuestros proyectos deberá tener un único trunk como línea base de desarrollo. Si tenemos más de un trunk, entonces algo no estamos haciendo bien… o tenemos un fork y no lo estamos viendo, o son branches tratados como trunk.

· En el trunk no deberemos hacer cambios con un impacto alto, o larga duración, ya que en caso de tener que pensar en armar una entrega, la solución podría no compilar o no ser estable, afectando la calidad del producto final. Los grandes cambios requieren tiempo de estabilización y ejecutar pruebas para validar que todo siga funcionando correctamente, y esto no es algo que podamos hacer de un día para el otro.

· El trunk siempre debe compilar y pasar todas las pruebas unitarias, de integración y de regresión en todo momento. Para lograr este objetivo podemos apoyarnos en alguna herramienta de Integración Continua, como Jenkins por ejemplo, para que nos ayude a correr los tests.

· Si algún miembro del equipo rompe el trunk, es decir, que la solución deja de compilar, se lo suele penalizar con el castigo de tener que traer algo para desayunar al día siguiente… por ejemplo, unas ricas facturas. Esto se suele arreglar con el equipo al comenzar el proyecto, y es interesante ya que todos vamos a estar pendientes de que el trunk esté estable en todo momento, tanto para no tener que traer facturas para todo el equipo, como para cazar a la persona que lo rompió, exigirle el pago y disfrutar de un buen desayuno al día siguiente.

A la hora de gestionar y administrar los diferentes branches, vale la pena aclarar que existen tres grandes estrategias a considerar. Elegir entre una u otra dependerá en gran medida del tipo de proyecto, y de la fase de vida en la que se encuentre (no es lo mismo uno que recién arranca que otro que ya se encuentra operativo y evolucionando continuamente). Las tres estrategias que podemos adoptar son:

· The Never-Branch Strategy

o Generalmente se emplea en proyectos que recién están comenzando y aún no tienen una aplicación funcionando en un ambiente del cliente (productivo o pre-productivo).

o Los desarrolladores trabajan directamente sobre el trunk, lo cual a veces genera que el mismo deje de compilar y/o pasar las pruebas definidas.

· The Always-Branch Strategy

o En el otro extremo, se encuentra la estrategia de generar siempre un branch por cada nueva tarea de codificación que haya que realizar, no importa su complejidad, no importa su duración o esfuerzo. Solamente cuando la tarea se haya realizado completamente y esté correctamente validada, se pasarán los cambios al trunk.

o Se la suele utilizar en proyectos complejos, o en donde el equipo no ha participado en la fase primaria del desarrollo y existe el riesgo de que un cambio desestabilice el resto de la aplicación, o en proyectos en donde existe un management muy exigente.

· The Branch-When-Needed Strategy

o En el medio de las dos estrategias anteriores, se encuentra la estrategia de realizar un nuevo branch sólo si es necesario, generalmente para los cambios más complejos o con un alto impacto en el resto de la aplicación.

o Es la estrategia favorita para los proyectos que ya se encuentren operativos y sobre los cuales tengan que convivir diferentes versiones del mismo: la versión productiva, las versiones pre-productivas y las versiones futuras con nuevos desarrollos o modificaciones.

o Desde mi punto de vista, es la más sensata y la que suelo usar en mis proyectos a partir del primer release de la aplicación a un ambiente del cliente, independientemente de si vaya a producción o simplemente quede en un ambiente de QA previo.

Si optamos, entonces, por implementar las estrategias que poseen branches, es bueno que sepan que existen tres diferentes tipos de branches que podemos crear en el repositorio:

· Feature Branches: O también conocidos como Ramas de Soporte o Ramas de Funcionalidad. Son aquellos que se crean de forma puntual para desarrollar una funcionalidad específica; y una vez estabilizada se sincroniza con lo que está en el trunk.

· Hotfix Branches: Se utilizan en los casos en que tenemos que generar un hotfix para corregir un error de forma urgente, y que suponemos que va a tener un alto impacto en nuestro código y por eso no lo hacemos en el trunk.

· Release Branches: Se crean para poder dar soporte a la salida de una nueva versión de producción de la aplicación. Permiten tener bajo control los entregables de la versión y poder realizar el mantenimiento de estabilización y de corrección sobre ella.

Considerando los dos puntos anteriores, entonces paso a compartirles algunas buenas prácticas para que trabajemos correctamente con Branches sin morir en el intento:

· Idealmente, para cada tarea compleja, que tenga un alto impacto, que sea de larga duración, y/o que aún no sabemos cuándo debemos disponibilizarla al cliente, deberemos trabajarla en un branch aparte para no perjudicar la estabilidad del trunk y de nuestra aplicación en general.

· Integrar en el trunk una funcionalidad desarrollada en un branch únicamente cuando esté totalmente desarrollada y testeada. No hacer integraciones parciales, ya que esto sólo va a terminar afectando la calidad final del producto.

· Tener la funcionalidad desarrollada en branches aparte nos permite, más allá de tenerla completamente desarrollada y validada, la posibilidad de elegir cuándo deseamos integrarla al trunk para liberarla en un próximo release.

· Si vamos a experimentar con nuestra aplicación, por ejemplo realizando alguna PoC (Proof of Concept) probando un nuevo componente o validando un upgrade de algún framework, hagámoslo sí o sí en un branch aparte para no ensuciar el trunk.

· Por cada release que necesitemos preparar para entregar al cliente, creemos un branch aparte, de forma de poder trabajar tranquilamente sobre él, realizando la estabilización final del producto antes de realizar el delivery.

· Sobre los branches de release no se deberán realizar tareas como refactor, code cleaning, etc. Sólo estarán permitidas pequeñas correcciones en código, pero no grandes modificaciones. Esto es en pos de facilitar luego el merging de los fuentes con el trunk.

· Otra de las ventajas de tener branches de release es que nos van a permitir efectuar correcciones en paralelo al desarrollo que se encuentre en el trunk y liberar versiones intermedias antes del siguiente major release a producción.

· Dado que con el tiempo nuestro repositorio va a tender a llenarse de branches, es bueno mantenerlo organizado desde un comienzo. Una buena práctica consiste en agrupar los diferentes tipos de branches. Para ello, se podrán nombrar los diferentes branches agregando un prefijo que indique su tipo; o se podrán crear subcarpetas por cada uno de los tipos (por ejemplo, en TFS esto es posible y queda mucho más limpio). Otra recomendación es seguir una nomenclatura estandarizada y conocida por todo el equipo del proyecto.

Y por último, en lo que respecta al armado de los tags, les propongo seguir las buenas prácticas que enumeraremos a continuación, lo que les facilitará enormemente encontrar y bajarse un determinado hito de la aplicación en un futuro:

· Primero y antes que nada, sobre un tag no deberemos realizar jamás cambios y subirlos al repositorio. Un tag es una versión congelada de la aplicación y no deberá modificarse.

· Realizando una analogía, veamos al tag como un branch en el que los archivos correspondientes al código fuente no evolucionan, sino que permanecen congelados.

· Realizar un tag por cada release de la aplicación que se realice, independientemente de si se trate de un major release o de uno intermedio.

· Realizar un tag siempre que consideremos que vamos a necesitar recuperar el estado actual del repositorio en un futuro, por ejemplo, es útil si en un branch queremos implementar una mejora, pero no estamos seguros de si finalmente la vamos a conservar.

· Definitivamente, es una muy buena práctica crear un tag antes y después de realizar un merge entre diferentes branches o desde un branch al trunk.

· Definir una nomenclatura estándar y darla a conocer a todo el equipo para nominar los diferentes tags en nuestro repositorio.

Bueno, espero que les haya resultado interesante el artículo, que les sean útiles los tips anteriores y que puedan aplicarlos en el día a día en sus respectivos proyectos.

De la misma forma que les comentaba en artículos anteriores, no existe una solución mágica, ni una única forma de resolver las cosas. El manejo del repositorio SCM no es la excepción, y si queremos mantenerlo organizado y funcional, deberemos utilizar nuestro criterio para determinar cuál es la mejor estrategia de branching, y principalmente, cuándo branchear y cuándo no, dependiendo en gran medida del contexto del proyecto y de las tareas que tengamos que realizar.

Ing. Ariel Martín Bensussán

.Net Practice Manager