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.

5 comentarios:

  1. Muy completa esta parte del tutorial muchas gracias.

    Esperamos las siguientes.

    ResponderEliminar
  2. mmm... weird. I saw this tutorial already... look at this url:

    Hands On ASP.NET MVC
    "...In this first season David Findley will demonstrate how to build Twitter's web-based interface using ASP.NET MVC..."

    http://www.learnvisualstudio.net/content/series/Hands_On_ASPDotNet_MVC.aspx

    ResponderEliminar
  3. I choose twitter because it is well know.
    I think It's like when you see the Norhwind database in a lot of tutorials.

    ResponderEliminar
  4. Definitely, you are right!. Well done man, congratulations, great blog.
    P.S. Cute little girl.

    ResponderEliminar