如何測試(一)

單元測試

會偽造一個與原本相同結構的類別,達到隔離外部資料,又能快速測試的效果。

偽造類別的使用時機

偽造類別可以回傳固定的資料,方便做以下的測試:

  • 測試每次都會變動的結果
  • 不容易被測試的問題(例:網路錯誤)
  • 要讀取外部資源(例: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
    }
}

results matching ""

    No results matching ""