Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 34 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
34
Dung lượng
206,74 KB
Nội dung
Developing the Login User Story The first user story that we are going to develop is Login. In iteration planning (Chapter 12), this story was broken down into the following tasks: • Create login screen • Create text entry fields for username and password • Build query to database to validate username and password • Determine login request success or failure Before you begin, you need to clean up the Northwind solution you created in Appendix A. Delete any empty (default) classes that were autogenerated by the IDE (Class1.cs) or web pages (WebPage1.aspx or Default.aspx) and any test classes you created (CategoryTests.cs and the associated Category.cs class). If you did not create the Northwind solution in Appendix A, do that now. The first task that you are going to work on is the one to validate the username and pass- word against the database. ■Note The source code presented here is not meant to necessarily show the best way to code a .NET web application. The source code is intentionally kept as basic and simple as possible, so that you can focus on the XP technique of developing software in a .NET environment. Apress has many great books on C# and the .NET Framework that you can refer to for thorough coverage of those topics. Build Query to Database to Validate Username and Password Task The Login user story has one business class (User.cs) and one data class (UserData.cs) that need unit tests. You are going to code iteratively, so you will start with the smallest unit test possible. Using the test-driven development approach, you start with the UserTests.cs file shown in Listing 13-1, which you need to add to the TestLayer project. You will need to add a refer- ence to the BusinessLayer and DataLayer projects on the TestLayer project, if you have not done so already. Listing 13-1. UserTests.cs File #region Using directives using System; using System.Collections.Generic; using System.Text; using NUnit.Framework; using BusinessLayer; using DataLayer; #endregion CHAPTER 13 ■ FIRST ITERATION 151 4800ch13.qrk 5/22/06 1:57 PM Page 151 namespace TestLayer { [TestFixture] public class UserTests { public UserTests() { } [SetUp] public void Init() { } [TearDown] public void Destroy() { } [Test] public void TestGetUser() { UserData userData = new UserData(); Assert.IsNotNull(userData.GetUser("bogususer", "password"), "GetUser returned a null value, gasp!"); } } } If you build the solution now, you will get several errors because your web application does not have a concept of a UserData class. To address that issue, you will need to define a minimal UserData class so you can successfully build but not pass the test. Listing 13-2 shows the minimal UserData.cs file that needs to be added to the DataLayer project. You will need to add a reference to the BusinessLayer project on the DataLayer project, if you have not done so already. Listing 13-2. Minimal UserData.cs File #region Using directives using System; using System.Collections.Generic; using System.Text; using BusinessLayer; #endregion CHAPTER 13 ■ FIRST ITERATION152 4800ch13.qrk 5/22/06 1:57 PM Page 152 namespace DataLayer { public class UserData { public UserData() { } public User GetUser(string username, string password) { User user = null; return user; } } } Set the TestLayer project as the startup project and build the solution. You still get com- piler errors—although the UserData class is now defined, you introduced another class (User) that is yet to be defined Listing 13-3 shows the minimal source for the User.cs class that needs to be added to the BusinessLayer project. Listing 13-3. Minimal User.cs File #region Using directive using System; using Sytem.Collections.Generic; using System.Text; #endregion namespace BusinessLayer { public class User { public User() { } } } CHAPTER 13 ■ FIRST ITERATION 153 4800ch13.qrk 5/22/06 1:57 PM Page 153 USING A MOCK OBJECT If the database had not been ready when you started coding this portion of the user story, you could have used a mock object here instead. To do that, you would first add a reference to the NMock DLL (nmock.dll) to the TestLayer project. Next, you would create an interface class called IUserData.cs that looks like the following. #region Using directives using System; using System.Collections.Generic; using System.Text; using BusinessLayer; #endregion namespace DataLayer { interface IuserData { User GetUser(string username, string password); } } Then you would make the UserTests.cs class look like the following. #region Using directives using System; using System.Collections.Generic; using System.Text; using NUnit.Framework; using NMock; using BusinessLayer; using DataLayer; #endregion namespace TestLayer { [TestFixture] public class UserTests { public UserTests() { } [SetUp] public void Init() { } CHAPTER 13 ■ FIRST ITERATION154 4800ch13.qrk 5/22/06 1:57 PM Page 154 [TearDown] public void Destroy() { } [Test] public void TestGetUser() { DynamicMock userData = new DynamicMock (typeof(IUserData)); Assert.IsNotNull(userData.GetUser("bogususer", "password"), "GetUser returned a null value, gasp!"); } } } When the database became available, you would implement the UserData.cs class as shown in Listing 13-2 and have the UserData class inherit (implement) the IUserData interface. At that time, you would also update the UserTests.cs class to use the UserData.cs class instead of the mock object you implemented. Now rebuild and run the solution. You will not get any compiler errors, but when the test executes, the test fails. That’s because you are simply returning a null value for the user. Let’s fix that first. Start by modifying the UserData.cs file as shown in Listing 13-4. Listing 13-4. Modified UserData.cs File #region Using directives using System; using System.Collections.Generic; using System.Text; using BusinessLayer; #endregion namespace DataLayer { public class UserData { public UserData() { } CHAPTER 13 ■ FIRST ITERATION 155 4800ch13.qrk 5/22/06 1:57 PM Page 155 public User GetUser(string username, string password) { User user = null; user = new User(); return user; } } } Notice that you just wrote a test, coded a little, and then refactored. This is the coding habit you want to develop. Once you have adopted this style of coding, you will find that you will produce fewer bugs and have a greater sense that the quality of the code you are creating is continually getting better. Now when you rebuild the solution, it builds just fine and your test passes, but nothing of any significance is really happening. To take the next step, you need to modify your UserData class to connect to the Northwind database to get the user’s role, along with the username and password, using the username and password passed to the UserData class from the UserTests class. Listing 13-5 shows the UserData.cs file with these changes. Listing 13-5. UserData.cs File Modified to Connect to the Database #region Using directives using System; using System.Collections.Generic; using System.Data; using System.Data.Odbc; using System.Text; using BusinessLayer; #endregion namespace DataLayer { public class UserData { private static string connectionString = "Driver={Microsoft Access Driver (*.mdb)};" + "DBQ=c:\\xpnet\\database\\Northwind.mdb"; public UserData() { } public User GetUser(string username, string password) { User user = null; CHAPTER 13 ■ FIRST ITERATION156 4800ch13.qrk 5/22/06 1:57 PM Page 156 try { OdbcConnection dataConnection = new OdbcConnection(); dataConnection.ConnectionString = connectionString; dataConnection.Open(); OdbcCommand dataCommand = new OdbcCommand(); dataCommand.Connection = dataConnection; // Build command string StringBuilder commandText = new StringBuilder("SELECT * FROM Users WHERE UserName='"); commandText.Append(username); commandText.Append("' AND Password='"); commandText.Append(password); commandText.Append("'"); dataCommand.CommandText = commandText.ToString(); OdbcDataReader dataReader = dataCommand.ExecuteReader(); // Make sure that we found our user if ( dataReader.Read() ) { user = new User(dataReader.GetString(0), dataReader.GetString(1)); } dataConnection.Close(); } catch(Exception e) { Console.WriteLine("Error: " + e.Message); } return user; } } } When you rebuild the solution now, you have a compile error because you don’t have a User class that has a constructor that takes arguments. Listing 13-6 shows the change to the User class. CHAPTER 13 ■ FIRST ITERATION 157 4800ch13.qrk 5/22/06 1:57 PM Page 157 Listing 13-6. Modified User.cs File #region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace BusinessLayer { public class User { private string userName; private string password; public User() { } public User(string userName, string password) { this.userName = userName; this.password = password; } } } Lastly, you need to enhance the UserTests class to pass a username and password to the UserData class. Since this is a test, you need to create test data that you feel confident will not exist in the database. That way, you can set up the data and remove it when your test has com- pleted safely. You will refactor the database connections better later, but for now, you will take the simplest approach possible. Listing 13-7 shows the modifications to the UserTests.cs file. Listing 13-7. Modified UserTests.cs File #region Using directives using System; using System.Collections.Generic; using System.Data; using System.Data.Odbc; using System.Text; using NUnit.Framework; using BusinessLayer; using DataLayer; #endregion CHAPTER 13 ■ FIRST ITERATION158 4800ch13.qrk 5/22/06 1:57 PM Page 158 namespace TestLayer { [TestFixture] public class UserTests { private StringBuilder connectionString; public UserTests() { // Build connection string connectionString = new StringBuilder("Driver={Microsoft Access Driver (*.mdb)}"); connectionString.Append(";DBQ=c:\\xpnet\\database\Northwind.mdb"); } [SetUp] public void Init() { try { OdbcConnection dataConnection = new OdbcConnection(); dataConnection.ConnectionString = connectionString.ToString(); dataConnection.Open(); OdbcCommand dataCommand = new OdbcCommand(); dataCommand.Connection = dataConnection; // Build command string StringBuilder commandText = new StringBuilder("INSERT INTO Users (UserName, Password"); commandText.Append(" VALUES ('bogususer', 'password')"); dataCommand.CommandText = commandText.ToString(); int rows = dataCommand.ExecuteNonQuery(); // Make sure that the INSERT worked Assert.AreEqual(1, rows, "Unexpected row count returned."); dataConnection.Close(); } catch(Exception e) { Assert.Fail("Error: " + e.Message); } } CHAPTER 13 ■ FIRST ITERATION 159 4800ch13.qrk 5/22/06 1:57 PM Page 159 [TearDown] public void Destroy() { try { OdbcConnection dataConnection = new OdbcConnection(); dataConnection.ConnectionString = connectionString.ToString(); dataConnection.Open(); OdbcCommand dataCommand = new OdbcCommand(); dataCommand.Connection = dataConnection; // Build command string StringBuilder commandText = new StringBuilder("DELETE FROM Users WHERE username='bogususer'"); dataCommand.CommandText = commandText.ToString(); int rows = dataCommand.ExecuteNonQuery(); // Make sure that the DELETE worked Assert.AreEqual(1, rows, "Unexpected row count returned"); dataConnection.Close(); } catch(Exception e) { Assert.Fail("Error: " + e.Message); } } [Test] public void TestGetUser() { UserData userData = new UserData(); Assert.IsNotNull(userData.GetUser("bogususer", "password"), "GetUser returned a null value, gasp!"); } } } Rebuild the solution again and run your tests. If you get any errors, look at the build or runtime output to see where the error occurred. Don’t forget that testing is not just all “happy day” scenarios. Add a negative test where you pass in a bad username and password, as shown in Listing 13-8. In this test, you should expect to get a null user back, since the user should not exist in the database. CHAPTER 13 ■ FIRST ITERATION160 4800ch13.qrk 5/22/06 1:57 PM Page 160 [...]... [TestFixture] public class ProductTests { private int categoryID; public ProductTests() { } [SetUp] public void Init() { } 175 4800ch13.qrk 1 76 5/22/ 06 1:57 PM Page 1 76 CHAPTER 13 ■ FIRST ITERATION [TearDown] public void Destroy() { } [Test] public void TestGetProductsByCategory() { ArrayList products = ProductData.GetProductsByCategory(categoryID); Assert.IsNotNull(products, "GetProductsByCategory returned... product.ProductName = dataReader.GetString(1); product.CategoryID = dataReader.GetInt32(3); product.Price = dataReader.GetDecimal(5); product.Quantity = dataReader.GetInt 16( 6); products.Add(product); } dataConnection.Close(); } catch(Exception e) { Console.WriteLine("Error: " + e.Message); } return products; } } } Next, create the Product.cs class shown in Listing 13-23 in the BusinessLayer project... public class ProductData { public ProductData() { } public static ArrayList GetProductsByCategory(int categoryID) { ArrayList products = null; return products; } } } 4800ch13.qrk 5/22/ 06 1:57 PM Page 177 CHAPTER 13 ■ FIRST ITERATION As with the Login user story, you should be able to build and run the solution for this story (assuming you have set the TestLayer project as the startup project) The ProductTests... string productName, decimal price, int quantity) { this.productID = productID; this.categoryID = categoryID; this.productName = productName; this.price = price; this.quantity = quantity; } public int ProductID { get { return this.productID; } set { this.productID = value; } } public int CategoryID { get { return this.categoryID; } set { this.categoryID = value; } } 179 4800ch13.qrk 180 5/22/ 06 1:57... [Test] public void TestGetProductsByCategory() { ArrayList products = ProductData.GetProductsByCategory(categoryID); Assert.IsNotNull(products, "GetProductsByCategory returned a null value, gasp!"); Assert.IsTrue(products.Count > 0, "Bad Products count, gasp!"); } } } Now everything should build and run successfully You should add some more tests (both positive and negative) to the ProductTests.cs class,... FROM Products WHERE CategoryID="); commandText.Append(categoryID); commandText.Append(" AND UnitsInStock > 0"); 177 4800ch13.qrk 178 5/22/ 06 1:57 PM Page 178 CHAPTER 13 ■ FIRST ITERATION dataCommand.CommandText = commandText.ToString(); OdbcDataReader dataReader = dataCommand.ExecuteReader(); while (dataReader.Read()) { Product product = new Product(); product.ProductID = dataReader.GetInt32(0); product.ProductName... 13-23 Product.cs File #region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace BusinessLayer { public class Product { private int productID; private int categoryID; private string productName; private decimal price; private int quantity; 4800ch13.qrk 5/22/ 06 1:57 PM Page 179 CHAPTER 13 ■ FIRST ITERATION public Product() { } public Product(int productID,... commandText = new StringBuilder("SELECT ProductID FROM"); commandText.Append(" Products WHERE ProductName = "); commandText.Append("'Bogus Product'"); dataCommand.CommandText = commandText.ToString(); OdbcDataReader dataReader = dataCommand.ExecuteReader(); // Make sure that we found our product if (dataReader.Read()) { productID = dataReader.GetInt32(0); } 4800ch13.qrk 5/22/ 06 1:57 PM Page 183 CHAPTER 13 ■... Retrieve Products Associated with a Category Task The categories are mapped to multiple products In order to retrieve the list of products associated with the categories, you need to get them from the database using the category selected Start by adding a new test class (ProductTests.cs) to the TestLayer project Listing 13-20 shows the minimal source for the ProductTests.cs class Listing 13-20 Minimal ProductTests.cs... runat="server"> Northwind Login 163 4800ch13.qrk 164 5/22/ 06 1:57 PM Page 164 CHAPTER 13 ■ FIRST ITERATION . style="z-index: 104 ; left: 427 px; position: absolute; top: 56px" Runat="server">Northwind Login</asp:Label> CHAPTER 13 ■ FIRST ITERATION 163 4 800 ch13.qrk 5 /22 / 06 1:57 PM Page 163 <asp:Button. style="z-index: 104 ; left: 427 px; position: absolute; top: 56px" Runat="server">Northwind Login</asp:Label> CHAPTER 13 ■ FIRST ITERATION 164 4 800 ch13.qrk 5 /22 / 06 1:57 PM Page 164 <asp:Label. the database query and set the success label as appropriate, as shown in Listing 13-14. CHAPTER 13 ■ FIRST ITERATION 166 4 800 ch13.qrk 5 /22 / 06 1:57 PM Page 166 Listing 13-14. Further Modifications to Login.aspx.cs using