miércoles, abril 29, 2009

ASP.NET MVC MyTwitter (Parte 2)

Como parte de la serie sobre el desarrollo de una aplicación similar a Twitter para aprender ASP.NET MVC. Voy a seguir con la parte de registro (Signup). Esta semana he estado ocupado con trabajo, por eso agregaré poca funcionalidad en este post, la cual consiste en que una vez que el usuario se registre debemos de iniciar sesión con ese usuario, es decir identificarlo como un usuario Autentificado.

Para ello es necesario escribir una clase (y una Interfaz) que me ayude en esta funcionalidad, para después hacer un mock de ella en el test del AccountController.

namespace MyTwitter.Models
{
public interface IFormsAuthenticationTasks
{
void SetAuthCookie(string username, bool createPersistentCookie);
void SignOut();
}
}
using System.Web.Security;
namespace MyTwitter.Models
{
public class FormsAuthenticationTasks
: MyTwitter.Models.IFormsAuthenticationTasks
{
public void SetAuthCookie(string username,
bool
createPersistentCookie)
{
FormsAuthentication.SetAuthCookie(
username, createPersistentCookie);
}

public void SignOut()
{
FormsAuthentication.SignOut();
}
}
}

agrego una variable de tipo IFormsAuthenticationTasks al AccountController y al constructor usado para las pruebas

public class AccountController : Controller
{
private IUserRepository userRepository;
private IFormsAuthenticationTasks formsAuthenticationTasks;

public AccountController(IUserRepository userRepository,
IFormsAuthenticationTasks formsAuthenticationTasks)
{
this.userRepository = userRepository ?? new UserRepository();
this.formsAuthenticationTasks =
formsAuthenticationTasks ?? new FormsAuthenticationTasks();
}

public AccountController()
: this(null, null)
{
}
...

escribo el test para la nueva funcionalidad

[TestMethod]
public void Login_User_After_Signup()
{
formsAuthenticationMock.Setup(
fa => fa.SetAuthCookie(It.IsAny<string>(), It.IsAny<bool>()));
controller.Signup(null);
formsAuthenticationMock.VerifyAll();
}

implemento la funcionalidad en el método Signup del Account Controller para que la prueba pase, es solo agregar una línea antes del redirect

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Signup(FormCollection formValues)
{
var user = new User();
UpdateModel(user);
userRepository.Add(user);
userRepository.Save();

formsAuthenticationTasks.SetAuthCookie(user.Username, true);

return Redirect("/invitations");
}
Hasta aquí con esta parte. Estuve tentado a llamarla “Parte 1.5” en lugar de “Parte 2”, por el poco contenido, pero se iba a empezar a complicar la numeración, continuaré con la serie en siguientes posts.

viernes, abril 24, 2009

ASP.NET MVC MyTwitter (Parte 1)

En la parte 0 de esta serie sobre ASP MVC MyTwitter hice la base de datos (la cual cambie un poco desde el pasado post) para iniciar el desarrollo de la aplicación. Ahora voy a generar el modelo de datos (LINQ to SQL classes) para eso en el folder de Models agrego el modelo de datos.

image

Le doy clic con el botón derecho en el folder de Models y le hago clic en “Add/New Item” y selecciono “LINQ to SQL Classes” y le pongo el nombre MyTwitter.dbml

image Arrastro desde el server explorer las tabla de la base de datos al modelo de datos, para que mi modelo quede así.

Cambie el nombre de la tabla UserFriends por Friends y la columna Friend por FriendName. Esto fue con el propósito de que quedaran con mejor nombre las clases generadas por LinqToSql.

Ahora empiezo con la pagina de inicio, esta seria solo un formulario para iniciar sesión y un link para registrarse si es que no se tiene una cuenta. Para ello abro la página Views/Home/Index.aspx y escribo el la forma en el HTML

<form action="/account/login" method="post">
<
fieldset>
<
p>
<
label for="username">Usuario</label>
<%= Html.TextBox("username") %>
<%= Html.ValidationMessage("username", "*") %>
</p>
<
p>
<
label for="password">Contrase&ntilde;a</label>
<%= Html.Password("password") %>
<%= Html.ValidationMessage("password", "*") %>
</p>
<
p>
<
input type="submit" value="Ingresar" />
</
p>
<
p>Si no tienes una cuenta registrate <a href="/signup">aqui</a>.</p>
</
fieldset>
</
form>

De igual manera también modifico la prueba unitaria que viene por omisión en nuestro proyecto de test (MyTwitter.Tests) dentro del archivo HomeControllerTest.cs.

[TestMethod]
public void Index()
{
HomeController controller = new HomeController();
var result = controller.Index();
Assert.IsNotNull(result);
}

Solo pruebo que el método Index() del HomeController regrese un View que no sea null.

Para realizar el registro de usuario voy a agregar un controller llamado AccountController, similar al que venia por default al crear al proyecto.


image



Hago clic en el folder Controllers y selecciono la opción “add controller”. y le pongo el nombre de “AccountController”.

Esto me agrega una nueva clase AccountController que hereda de la clase Controller con un método llamado Index. por el momento elimino el método Index, ya que todavía no lo voy a usar (casi no me gusta tener código que no hace nada) y agrego un método llamado Signup().

namespace MyTwitter.Controllers
{
public class AccountController : Controller
{
public ActionResult Signup()
{
throw new NotImplementedException();
}
}
}
Ahora el unit test

image



En el folder Controllers del proyecto MyTwitter.Tests selecciono “Add > New Test..” y selecciono “Unit Test” y le doy el nombre de AccountControllerTest. Esto me agrega una clase con código de ejemplo.

Quito el código que genera para quedar con una prueba sencilla que verifica que se regrese un view para realizar el registro.

[TestMethod]
public void Return_A_View_For_Signup()
{
var result = controller.Signup();
Assert.IsNotNull(result);
}
Implemento el método de Signup en el AccountController, este simplemente (como el test lo indica) regresará el view necesario para que el usuario ingrese sus datos de registro
public ActionResult Signup()
{
return View();
}
Para agregar el View abro el menú contextual sobre el método Signup y selecciono “Add View”
image

image



Aparece un dialogo para agregar un view. aquí habilito la opción “Create a strongly-typed view” y selecciono (en “View data class:”) la clase MyTwitter.Models.User, En “View content” selecciono la opción Create. Esto para que VisualStudio escriba la mayoría del HTML por mi. Esto agrega un archivo (Views\Account\Signup.aspx) , lo abre en el editor y lo modifico para que quede en español.

El view queda así:

<% using (Html.BeginForm()) {%>

<fieldset>
<
legend>Fields</legend>
<
p>
<
label for="Username">Usuario:</label>
<%= Html.TextBox("Username") %>
<%= Html.ValidationMessage("Username", "*") %>
</p>
<
p>
<
label for="FullName">Nombre Completo:</label>
<%= Html.TextBox("FullName") %>
<%= Html.ValidationMessage("FullName", "*") %>
</p>
<
p>
<
label for="Password">Contraseña:</label>
<%= Html.Password("Password") %>
<%= Html.ValidationMessage("Password", "*") %>
</p>
<
p>
<
label for="ConfirmPassword">Confirmar Contraseña:</label>
<%= Html.Password("ConfirmPassword") %>
<%= Html.ValidationMessage("ConfirmPassword", "*") %>
</p>
<
p>
<
label for="Email">Correo Electronico:</label>
<%= Html.TextBox("Email") %>
<%= Html.ValidationMessage("Email", "*") %>
</p>
<
p>
<
label for="Url">Sitio Web:</label>
<%= Html.TextBox("Url") %>
<%= Html.ValidationMessage("Url", "*") %>
</p>
<
p>
<
label for="Bio">Biografia:</label>
<%= Html.TextBox("Bio") %>
<%= Html.ValidationMessage("Bio", "*") %>
</p>
<
p>
<
label for="Location">Ubicacion:</label>
<%= Html.TextBox("Location") %>
<%= Html.ValidationMessage("Location", "*") %>
</p>
<
p>
<
input type="submit" value="Create" />
</
p>
</
fieldset>

<% } %>
Las funciones de la clase Html me ayuda a no tener que escribir todo el HTML a pie (o mejor dicho a mano), no explicare como funcionan (creo que el nombre de cada método describe bien lo que hace) basta decir que escriben HTML. Uso Html.BeginForm sin parámetros en lugar de escribir el HTML para un <form> y así el action es al mismo url actual pero con el method=”post”. en este caso seria equivalente a escribir <form action=”/signup” method=”post”> y al cerrar el using (<% } %>) se escribe el </form>.

Ahora voy a implementar el código que se ejecutará cuando el usuario haga submit a esta forma, para esto creo un nuevo método en el AccountController llamando también Signup la diferencia con el otro son los parámetros que recibe (en este caso los valores de la forma) y un atributo para que solo reciba peticiones de tipo POST.

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Signup(FormCollection formValues)
{
throw new NotImplementedException();
}

Antes de implementar la función quiero escribir un test para probar que el método Signup trate de guardar la información en la base de datos, pero sin hacer una llamada real a la base de datos, para esto voy a crear una clase UserReposotory que será la que se encargue de cargar los datos. También voy a crear una Interfaz para poder hacer un mock del repository. Esta clase e interfaz las pongo dentro del folder Models cada una en su propio archivo.

namespace MyTwitter.Models
{
public interface IUserRepository
{
void Add(User user);
User GetUser(string username);
void Save();
}
}
namespace MyTwitter.Models
{
public class UserRepository : IUserRepository
{
private MyTwitterDataContext db;

public UserRepository()
{
db = new MyTwitterDataContext();
}

public User GetUser(string username)
{
return db.Users.FirstOrDefault(u => u.Username == username);
}

public void Add(User user)
{
db.Users.InsertOnSubmit(user);
}

public void Save()
{
db.SubmitChanges();
}
}
}
Ahora sí, ya puedo empezar a escribir mi prueba en AccountControllerTest. Para no tener que hacer uso de la base de datos en las pruebas unitarias utilizo Moq, el cual es un mock framework que se puede descargar aquí.

image Una vez descargado moq, agrego una reference al moq.dll en el proyecto MyTwitter.Tests y agrego el using Moq; en el AccountControllerTest.cs. Con la ayuda de este framework vamos probar que el AccountController llame el metodo Add y Save del UserRepository.

Para no repetir la inicialización del repositoryMock y del controller, voy a agregarlos como variables de clase e inicializarlos en un método SetUp

[TestClass]
public class AccountControllerTest
{
private AccountController controller;
private Mock<IUserRepository> repositoryMock;

[TestInitialize]
public void Setup()
{
repositoryMock = new Mock<IUserRepository>();
controller = new AccountController(repositoryMock.Object);
}
...
Ahora agrego los métodos de prueba
[TestMethod]
public void Add_And_Save_User_To_Repository()
{
repositoryMock.Setup(r => r.Add(It.IsAny<User>()));
repositoryMock.Setup(r => r.Save());

controller.Signup(new FormCollection());

repositoryMock.VerifyAll();
}

Además voy a agregar un test para probar que, una vez que se realice el registro del usuario, se envíe al usario a la pagina “/invitations” para que ahí el usuario pueda agregar amigos (friends) a quien seguir.

[TestMethod]
public void Redirect_To_Invitations_After_Signup()
{
var result = controller.Signup(new FormCollection()) as RedirectResult;

Assert.AreEqual("/invitations", result.Url);
}
Si corro el test ahora fallará porque no lo he implementado, entonces ahora lo que debo de hacer es que el test pase. Para ello declaro un IUserRepository en la clase AccountController y 2 constructores, uno que usare en el código en producción y el otro para las pruebas.
public class AccountController : Controller
{
private IUserRepository userRepository;

public AccountController(IUserRepository userRepository)
{
this.userRepository = userRepository ?? new UserRepository();
}

public AccountController()
: this(null)
{
}
...
ahora si, por fin implemento el método Signup en el AccountController
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Signup(FormCollection formValues)
{
var user = new User();
UpdateModel(user);
userRepository.Add(user);
userRepository.Save();

return Redirect("/invitations");
}
[tanto blog post para este método tan sencillo :) ]

image Ejecuto las pruebas unitarias y verifico que el código hace lo que se supone debe hacer.

Si ejecuto la aplicación y en la pagina de inicio hago clic en el enlace para registrase me manda a “/signup” y me marca error por que no encuentra el recurso. Para que al buscar el recurso /signup se ejecute el action Signup del AccountController, debo de agregar un Route en el archivo Global.asax ( lo agrego antes del Route “Default”).

public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Signup",
"signup",
new { controller = "Account", action = "Signup" });

routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);

}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}

con esto ya puedo hacer una pequeña prueba de integración.En la siguiente parte continuare con el desarrollo de esta aplicación.

miércoles, abril 22, 2009

ASP.NET MVC MyTwitter (Parte 0)

Para aprender acerca de ASP.NET MVC voy a realizar una aplicación de prueba que sería la versión en español de twitter (con esta aplicación solo pretendo aprender, no pretendo competir con twitter) que llamaré MyTwitter. Este post lo he titulado parte 0 de la serie por que solo escribiré el setup de la aplicación y a partir de la parte 1 empezare (ahora si) con el desarrollo.

Antes de iniciar debo instalar el ASP MVC Framework, el cual se puede descargar aquí.

 

image

Una vez que instalé el MVC Framework puedo crear un nuevo proyecto en visual studio (no es un Web Site, sino un project).

 

 

 

 image

Después selecciono la plantilla de ASP.NET MVC Web Application y le doy el nombre de MyTwitter

 

 

 

 

 

image

Una vez hecho esto aparece una ventana que me pregunta si quiero un proyecto para las pruebas unitarias. Seleccione la opción de “Yes”. Esto creará una aplicación con 2 controllers (Home y Account). Adema incluye algunas pruebas unitarias para el proyecto de ejemplo. Para este ejemplo voy a quitar el AccountController (y su respectivo test) para crear uno propio.

image

Ahora voy a crear la base de datos. Haciendo clic con el botón derecho en el folder App_Data. Esa opción aparece si se le da con el botón derecho en cualquier parte del proyecto pero me gusta hacerlo sobre el App_Data folder por que así me aparecen los ítems filtrados.

 

image

Selecciono SQL Server Database y le doy el nombre de MyTwitter.mdf. Una vez hecho esto me aparece la nueva base de datos bajo el folder de App_Data Dandole doble clic, me agrega una nueva conexión en el Server Explorer y la abre.

 

 

image

 

Ahora agrego algunas tablas para iniciar el desarrollo del sitio. Por lo pronto agregaremos  Users, Posts y UserFriends.

 

 

 

 

 

 

 

Con esto queda listo nuestro proyecto para empezar a agregarle funcionalidad. En el siguiente post (Parte 1) seguiré con el desarrollo de este proyecto de prueba “MyTwitter”.