如何測試(一)
單元測試
會偽造一個與原本相同結構的類別,達到隔離外部資料,又能快速測試的效果。
偽造類別的使用時機
偽造類別可以回傳固定的資料,方便做以下的測試:
- 測試每次都會變動的結果
- 不容易被測試的問題(例:網路錯誤)
- 要讀取外部資源(例:DB、文字檔)
測試工具(windows)
擴充工具(Visual Studio)
- NUnit Test Adapter
建立測試專案的共用類別庫
類別庫名稱:TestHelper
加入Nuget
- NUNit
加入專案參考
- DataModel
建立以下的檔案 :
DataInitializer.cs
建立測試用的靜態資料
using DataModel;
using System;
using System.Collections.Generic;
namespace TestHelper
{
/// <summary>
/// Data initializer for unit tests
/// </summary>
public class DataInitializer
{
/// <summary>
/// Dummy products
/// </summary>
/// <returns></returns>
public static List<Products> GetAllProducts()
{
var products = new List<Products>
{
new Products() {ProductName = "Laptop"},
new Products() {ProductName = "Mobile"},
new Products() {ProductName = "HardDrive"},
new Products() {ProductName = "IPhone"},
new Products() {ProductName = "IPad"}
};
return products;
}
/// <summary>
/// Dummy tokens
/// </summary>
/// <returns></returns>
public static List<Tokens> GetAllTokens()
{
var tokens = new List<Tokens>
{
new Tokens()
{
AuthToken = "9f907bdf-f6de-425d-be5b-b4852eb77761",
ExpiresOn = DateTime.Now.AddHours(2),
IssuedOn = DateTime.Now,
UserId = 1
},
new Tokens()
{
AuthToken = "9f907bdf-f6de-425d-be5b-b4852eb77762",
ExpiresOn = DateTime.Now.AddHours(1),
IssuedOn = DateTime.Now,
UserId = 2
}
};
return tokens;
}
/// <summary>
/// Dummy users
/// </summary>
/// <returns></returns>
public static List<User> GetAllUsers()
{
var users = new List<User>
{
new User()
{
UserName = "akhil",
Password = "akhil",
Name = "Akhil Mittal",
},
new User()
{
UserName = "arsh",
Password = "arsh",
Name = "Arsh Mittal",
},
new User()
{
UserName = "divit",
Password = "divit",
Name = "Divit Agarwal",
}
};
return users;
}
}
}
建立比對物件用的共用測試方法
ProductComparer.cs
比較兩個Proudcts物件是否一致
using DataModel;
using System;
using System.Collections;
using System.Collections.Generic;
namespace BusinessServices.Tests
{
public class ProductComparer : IComparer, IComparer<Products>
{
public int Compare(object expected, object actual)
{
var lhs = expected as Products;
var rhs = actual as Products;
if (lhs == null || rhs == null) throw new InvalidOperationException();
return Compare(lhs, rhs);
}
public int Compare(Products expected, Products actual)
{
int temp;
return (temp = expected.ProductId.CompareTo(actual.ProductId)) != 0 ? temp : expected.ProductName.CompareTo(actual.ProductName);
}
}
}
AssertObjects.cs
比對List或單一物件是否相同
using NUnit.Framework;
using System.Collections;
using System.Reflection;
namespace TestHelper
{
public static class AssertObjects
{
public static void PropertyValuesAreEquals(object actual, object expected)
{
PropertyInfo[] properties = expected.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
object expectedValue = property.GetValue(expected, null);
object actualValue = property.GetValue(actual, null);
if (actualValue is IList)
AssertListsAreEquals(property, (IList)actualValue, (IList)expectedValue);
else if (!Equals(expectedValue, actualValue))
if (property.DeclaringType != null)
Assert.Fail("Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue);
}
}
private static void AssertListsAreEquals(PropertyInfo property, IList actualList, IList expectedList)
{
if (actualList.Count != expectedList.Count)
Assert.Fail("Property {0}.{1} does not match. Expected IList containing {2} elements but was IList containing {3} elements",
property.PropertyType.Name,
property.Name, expectedList.Count, actualList.Count);
for (int i = 0; i < actualList.Count; i++)
if (!Equals(actualList[i], expectedList[i]))
Assert.Fail("Property {0}.{1} does not match. Expected IList with element {1} equals to {2} but was IList with element {1} equals to {3}", property.PropertyType.Name, property.Name, expectedList[i], actualList[i]);
}
}
}
建立測試專案
測試專案名稱:BusinessServices.Tests
Nuget
- Nunit
- Moq
- EntityFramework
- AutoMapper
加入專案參考
- BusinessServices
- BusinessEntities
- DataModel
- TestHelper
建立單元測試
ProductServicesTests.cs
與原文不同的地方:
- 改用新版本的AutoMapper
- 改用新版本的NUnit
- 每次測試(OneTime)都會重新初使化產品的靜態資料
- 移除第一次測試時(OneTimeSetUp)會初使化產品靜態資料的邏輯
using AutoMapper;
using BusinessEntities;
using DataModel;
using DataModel.UnitOfWork;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using TestHelper;
namespace BusinessServices.Tests
{
public class ProductServicesTests
{
#region Variables
private IProductServices _productService;
private IUnitOfWork _unitOfWork;
private List<Products> _products;
private GenericRepository<Products> _productRepository;
private WebApiDbEntities _dbEntities;
#endregion
#region Test fixture setup
#endregion
#region Setup
/// <summary>
/// Re-initializes test.
/// </summary>
[SetUp]
public void ReInitializeTest()
{
_products = SetUpProducts();
_dbEntities = new Mock<WebApiDbEntities>().Object;
_productRepository = SetUpProductRepository();
var unitOfWork = new Mock<IUnitOfWork>();
unitOfWork.SetupGet(s => s.ProductRepository).Returns(_productRepository);
_unitOfWork = unitOfWork.Object;
_productService = new ProductServices(_unitOfWork);
}
private GenericRepository<Products> SetUpProductRepository()
{
// Initialise repository
var mockRepo = new Mock<GenericRepository<Products>>(MockBehavior.Default, _dbEntities);
// Setup mocking behavior
mockRepo.Setup(p => p.GetAll()).Returns(_products);
mockRepo.Setup(p => p.GetByID(It.IsAny<int>()))
.Returns(new Func<int, Products>(
id => _products.Find(p => p.ProductId.Equals(id))));
mockRepo.Setup(p => p.Insert((It.IsAny<Products>())))
.Callback(new Action<Products>(newProduct =>
{
var maxProductID = _products.Last().ProductId;
var nextProductID = maxProductID + 1;
newProduct.ProductId = nextProductID;
_products.Add(newProduct);
}));
mockRepo.Setup(p => p.Update(It.IsAny<Products>()))
.Callback(new Action<Products>(prod =>
{
var oldProduct = _products.Find(a => a.ProductId == prod.ProductId);
oldProduct = prod;
}));
mockRepo.Setup(p => p.Delete(It.IsAny<Products>()))
.Callback(new Action<Products>(prod =>
{
var productToRemove =
_products.Find(a => a.ProductId == prod.ProductId);
if (productToRemove != null)
_products.Remove(productToRemove);
}));
// Return mock implementation object
return mockRepo.Object;
}
#endregion
private static List<Products> SetUpProducts()
{
var prodId = new int();
var products = DataInitializer.GetAllProducts();
foreach (Products prod in products)
prod.ProductId = ++prodId;
return products;
}
public IEnumerable<BusinessEntities.ProductEntity> GetAllProducts()
{
var products = _unitOfWork.ProductRepository.GetAll().ToList();
if (products.Any())
{
Mapper.Initialize(cfg => cfg.CreateMap<Products, ProductEntity>());
var productsModel = Mapper.Map<List<Products>, List<ProductEntity>>(products);
return productsModel;
}
return null;
}
[Test]
public void GetAllProductsTest()
{
var products = _productService.GetAllProducts();
var productList =
products.Select(productEntity => new Products
{
ProductId = productEntity.ProductId,
ProductName = productEntity.ProductName
}).ToList();
var comparer = new ProductComparer();
CollectionAssert.AreEqual(
productList.OrderBy(product => product, comparer),
_products.OrderBy(product => product, comparer), comparer);
}
/// <summary>
/// Service should return null
/// </summary>
[Test]
public void GetAllProductsTestForNull()
{
_products.Clear();
var products = _productService.GetAllProducts();
Assert.Null(products);
SetUpProducts();
}
/// <summary>
/// Service should return product if correct id is supplied
/// </summary>
[Test]
public void GetProductByRightIdTest()
{
var mobileProduct = _productService.GetProductById(2);
if (mobileProduct != null)
{
Mapper.Initialize(cf => cf.CreateMap<ProductEntity, Products>());
var productModel = Mapper.Map<ProductEntity, Products>(mobileProduct);
AssertObjects.PropertyValuesAreEquals(productModel,
_products.Find(a => a.ProductName.Contains("Mobile")));
}
}
/// <summary>
/// Service should return null
/// </summary>
[Test]
public void GetProductByWrongIdTest()
{
var product = _productService.GetProductById(0);
Assert.Null(product);
}
/// <summary>
/// Add new product test
/// </summary>
[Test]
public void AddNewProductTest()
{
var newProduct = new ProductEntity()
{
ProductName = "Android Phone"
};
var maxProductIDBeforeAdd = _products.Max(a => a.ProductId);
newProduct.ProductId = maxProductIDBeforeAdd + 1;
_productService.CreateProduct(newProduct);
var addedproduct = new Products() {
ProductName = newProduct.ProductName,
ProductId = newProduct.ProductId
};
AssertObjects.PropertyValuesAreEquals(addedproduct, _products.Last());
Assert.That(maxProductIDBeforeAdd + 1, Is.EqualTo(_products.Last().ProductId));
}
/// <summary>
/// Update product test
/// </summary>
[Test]
public void UpdateProductTest()
{
var firstProduct = _products.First();
firstProduct.ProductName = "Laptop updated";
var updatedProduct = new ProductEntity()
{
ProductName = firstProduct.ProductName,
ProductId = firstProduct.ProductId
};
_productService.UpdateProduct(firstProduct.ProductId, updatedProduct);
Assert.That(firstProduct.ProductId, Is.EqualTo(1)); // hasn't changed
Assert.That(firstProduct.ProductName, Is.EqualTo("Laptop updated")); // Product name changed
}
/// <summary>
/// Delete product test
/// </summary>
[Test]
public void DeleteProductTest()
{
int maxID = _products.Max(a => a.ProductId); // Before removal
var lastProduct = _products.Last();
// Remove last Product
_productService.DeleteProduct(lastProduct.ProductId);
Assert.That(maxID, Is.GreaterThan(_products.Max(a => a.ProductId))); // Max id reduced by 1
}
#region TestFixture TearDown.
/// <summary>
/// Tears down each test data
/// </summary>
[TearDown]
public void DisposeTest()
{
_productService = null;
_unitOfWork = null;
_productRepository = null;
if (_dbEntities != null)
_dbEntities.Dispose();
}
/// <summary>
/// TestFixture teardown
/// </summary>
[OneTimeTearDown]
public void DisposeAllObjects()
{
_products = null;
}
#endregion
}
}
TokenServiceTests.cs
與原文不同的地方:
- 覆寫Get(Func< TEntity,Bool>)的方法,避免ValidateTokenWithRightAuthToken方法無法測試通過
- 需把UnitOfWork的Get方法改為Virtual才能Mock
using DataModel;
using DataModel.UnitOfWork;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using TestHelper;
namespace BusinessServices.Tests
{
public class TokenServicesTests
{
private ITokenServices _tokenServices;
private IUnitOfWork _unitOfWork;
private List<Tokens> _tokens;
private GenericRepository<Tokens> _tokenRepository;
private WebApiDbEntities _dbEntities;
//測試案例用-授權過的Token
private const string SampleAuthToken = "9f907bdf-f6de-425d-be5b-b4852eb77761";
#region Test fixture setup
/// <summary>
/// Initial setup for tests
/// </summary>
[OneTimeSetUp]
public void Setup()
{
_tokens = SetUpTokens();
}
#endregion
#region Setup
/// <summary>
/// Re-initializes test.
/// </summary>
[SetUp]
public void ReInitializeTest()
{
_dbEntities = new Mock<WebApiDbEntities>().Object;
_tokenRepository = SetUpTokenRepository();
var unitOfWork = new Mock<IUnitOfWork>();
unitOfWork.SetupGet(s => s.TokenRepository).Returns(_tokenRepository);
_unitOfWork = unitOfWork.Object;
_tokenServices = new TokenServices(_unitOfWork);
}
private GenericRepository<Tokens> SetUpTokenRepository()
{
// Initialise repository
var mockRepo = new Mock<GenericRepository<Tokens>>(MockBehavior.Default, _dbEntities);
// Setup mocking behavior
mockRepo.Setup(p => p.GetAll()).Returns(_tokens);
mockRepo.Setup(p => p.Get(It.IsAny<Func<Tokens, bool>>()))
.Returns<Func<Tokens, bool>>( _where =>
{
return _tokens.Where(_where).FirstOrDefault();
});
mockRepo.Setup(p => p.GetByID(It.IsAny<int>()))
.Returns(new Func<int, Tokens>(
id => _tokens.Find(p => p.TokenId.Equals(id))));
mockRepo.Setup(p => p.GetByID(It.IsAny<string>()))
.Returns(new Func<string, Tokens>(
authToken => _tokens.Find(p => p.AuthToken.Equals(authToken))));
mockRepo.Setup(p => p.Insert((It.IsAny<Tokens>())))
.Callback(new Action<Tokens>(newToken =>
{
var maxTokenID = _tokens.Last().TokenId;
var nextTokenID = maxTokenID + 1;
newToken.TokenId = nextTokenID;
_tokens.Add(newToken);
}));
mockRepo.Setup(p => p.Update(It.IsAny<Tokens>()))
.Callback(new Action<Tokens>(token =>
{
var oldToken = _tokens.Find(a => a.TokenId == token.TokenId);
oldToken = token;
}));
mockRepo.Setup(p => p.Delete(It.IsAny<Tokens>()))
.Callback(new Action<Tokens>(prod =>
{
var tokenToRemove =
_tokens.Find(a => a.TokenId == prod.TokenId);
if (tokenToRemove != null)
_tokens.Remove(tokenToRemove);
}));
//Create setup for other methods too. note non virtauls methods can not be set up
// Return mock implementation object
return mockRepo.Object;
}
#endregion
/// <summary>
/// Setup dummy tokens data
/// </summary>
/// <returns></returns>
private static List<Tokens> SetUpTokens()
{
var tokId = new int();
var tokens = DataInitializer.GetAllTokens();
foreach (Tokens tok in tokens)
tok.TokenId = ++tokId;
return tokens;
}
[Test]
public void GenerateTokenByUserIdTest()
{
const int userId = 1;
var maxTokenIdBeforeAdd = _tokens.Max(a => a.TokenId);
var tokenEntity = _tokenServices.GenerateToken(userId);
var newTokenDataModel = new Tokens()
{
AuthToken = tokenEntity.AuthToken,
TokenId = maxTokenIdBeforeAdd + 1,
ExpiresOn = tokenEntity.ExpiresOn,
IssuedOn = tokenEntity.IssuedOn,
UserId = tokenEntity.UserId
};
AssertObjects.PropertyValuesAreEquals(newTokenDataModel, _tokens.Last());
}
/// <summary>
/// Validate token test
/// </summary>
[Test]
public void ValidateTokenWithRightAuthToken()
{
var authToken = Convert.ToString(SampleAuthToken);
var validationResult = _tokenServices.ValidateToken(authToken);
Assert.That(validationResult, Is.EqualTo(true));
}
[Test]
public void ValidateTokenWithWrongAuthToken()
{
var authToken = Convert.ToString("xyz");
var validationResult = _tokenServices.ValidateToken(authToken);
Assert.That(validationResult, Is.EqualTo(false));
}
#region Tear Down
/// <summary>
/// Tears down each test data
/// </summary>
[TearDown]
public void DisposeTest()
{
_tokenServices = null;
_unitOfWork = null;
_tokenRepository = null;
if (_dbEntities != null)
_dbEntities.Dispose();
}
#endregion
#region TestFixture TearDown.
/// <summary>
/// TestFixture teardown
/// </summary>
[OneTimeTearDown]
public void DisposeAllObjects()
{
_tokens = null;
}
#endregion
}
}