6 chuong 06

34 35 0
6 chuong 06

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

CHƯƠNG Công cụ cần thiết cho MVC Trong chương này, xem xét ba công cụ mà lập trình viên MVC cần phải có: dependency injection (DI) container, framework kiểm thử đơn vị, công cụ làm giả Tôi chọn ba triển khai cụ thể công cụ cho sách này, có nhiều lựa chọn thay khác cho loại Nếu bạn không quen thuộc với ci mà sử dụng, đừng nên lo lắng Chúng có nhiều, chắn bạn tìm thấy phù hợp với Như đề cập Chương 3, Ninject DI container ưa thích tơi Nó đơn giản, lịch dễ sử dụng Có nhiều lựa chọn tinh tế hơn, tơi thích cách mà Ninject làm việc mà khơng phải cấu hình q nhiều Nếu bạn khơng thích Ninject, tơi khun bạn nên thử Unity, thay từ Microsoft Đối với kiểm thử đơn vị, sử dụng công cụ kiểm thử xây dựng sẵn Visual Studio Tôi sử dụng NUnit, mà framework kiểm thử đơn vị NET phổ biến, Microsoft thực thúc đẩy lớn để cải thiện hỗ trợ kiểm thử đơn vị Visual Studio tích hợp phiên Visual Studio miễn phí Kết framework kiểm thử đơn vị tích hợp cách chặt chẽ vào toàn IDE điều tốt Các công cụ thứ ba chọn Moq, cơng cụ làm giả Tơi sử dụng Moq để tạo triển khai giao diện để sử dụng kiểm thử đơn vị Các lập trình viên thích ghét Moq; khơng có Hoặc bạn thấy thấy cú pháp lịch biểu cảm, khơng bạn bị nguyền rủa bạn cố gắng sử dụng Nếu bạn cảm thấy không quen, đề nghị thử Rhino Mocks, thay tốt Tơi giới thiệu cơng cụ trình bày tính cốt lõi chúng, không đào sâu cách đầy đủ chúng Chi tiết số chúng sách riêng Tơi cho bạn đủ thông tin để bắt đầu làm theo ví dụ phần lại sách Bảng 6-1 cung cấp tóm tắt cho chương Bảng 6-1 Tóm tắt Chương Vấn đề Giải pháp Tách lớp Giới thiệu giao diện khai báo phụ thuộc vào chúng lớp constructor Tự động xử lý phụ thuộc thể cách sử dụng giao diện Liệt kê 9, 13 16 10 Sử dụng Ninject DI container khác Tạo triển khai gian diện IDependencyResolver, gọi cho kernel Ninject đăng ký resolver cách gọi phương thức System.Web.Mvc.DependencyResolver.SetResolver 11, 12 Đưa giá trị property constructor vào đối tượng tạo Sử dụng phương thức WithPropertyValue WithConstructorArgument 17 Tự động chọn lớp triển khai cho giao diện Sử dụng ràng buộc có điều kiện Ninject 21, 22 Kiểm sốt vòng đời đối tượng mà Ninject tạo Thiết lập phạm vi đối tượng 23 Tạo kiểm thử đơn vị Thêm project kiểm thử đơn vị vào solution thích tập tin lớp với thuộc tính TestClass TestMethod 26, 27, 29, 30 Kiểm tra kết mong đợi kiểm thử đơn vị Sử dụng lớp Assert 28 Cô lập mục tiêu kiểm thử cách sử dụng đối tượng giả 31 Tích hợp Ninject vào ứng dụng MVC Tập trung kiểm thử đơn vị vào tính thành phần 128 128 20 25 34 Chú ý Chương giả định bạn muốn tất lợi ích có từ MVC Framework, bao gồm kiến trúc có hỗ trợ nhiều kiểm thử nhấn mạnh vào việc tạo ứng dụng dễ dàng sửa đổi bảo trì Tơi thích thứ này, tơi biết số độc giả muốn hiểu tính mà MVC Framework cung cấp không muốn biết phương pháp phương châm việc phát triển Tơi khơng cố gắng thay đổi bạn Đó định cá nhân bạn biết bạn cần làm để phát triển dự án Tơi đề nghị chí bạn nên xem lướt qua qua chương để xem có sẵn, bạn khơng phải người thích kiểm thử đơn vị, bạn nhảy tới chương xem làm để xây dựng ứng dụng MVC mẫu thực tế Chuẩn bị Project Mẫu Tôi bắt đầu cách tạo project mẫu đơn giản, tên EssentialTools , mà sử dụng suốt chương Tôi sử dụng template ASP.NET MVC Web Application, chọn tùy chọn Empty đánh dấu vào ô để thêm nội dung project MVC Tạo Các Lớp Model Tôi thêm tập tin lớp vào thư mục Models tên Product.cs thiết lập nội dung Liệt kê 6-1 Đây lớp model từ chương trước thay đổi namespace khớp với project EssentialTools Liệt kê 6-1 Nội dung tập tin Product.cs namespace EssentialTools.Models { public class Product { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { set; get; } } } Tôi muốn thêm lớp tính tổng giá đối tượng Product Tôi thêm tập tin lớp vào thư mục Models tên LinqValueCalculator.cs thiết lập nội dung khớp với Liệt kê 6-2 Liệt kê 6-2 Nội dung tập tin LinqValueCalculator.cs using System.Collections.Generic; using System.Linq; namespace EssentialTools.Models { public class LinqValueCalculator { public decimal ValueProducts(IEnumerable products) { return products.Sum(p => p.Price); } } } Lớp LinqValueCalculator định nghĩa phương thức tên ValueProducts , sử dụng phương thức Sum LINQ để cộng giá trị thuộc tính Price đối tượng Product thành số đếm (một tính tốt LINQ mà tơi thường sử dụng) Lớp model cuối ShoppingCart đại diện cho đối tượng Product sử dụng 129 129 LinqValueCalculator để xác định tổng giá trị Tôi tạo tập tin lớp tên ShoppingCart.cs thêm vào câu lệnh Liệt kê 6-3 Liệt kê 6-3 Nội dung tập tin ShoppingCart.cs using System.Collections.Generic; namespace EssentialTools.Models { public class ShoppingCart { private LinqValueCalculator calc; public ShoppingCart(LinqValueCalculator calcParam) { calc = calcParam; } public IEnumerable Products { get; set; } pr public decimal CalculateProductTotal() { return calc.ValueProducts(Products); } } } Thêm Controller Tôi thêm controller vào thư mục Controllers tên HomeController thiết lập nội dung khớp với Liệt kê 6-4 Phương thức hành động Index tạo mảng đối tượng Product sử dụng đối tượng LinqValueCalculator để xuất tổng giá trị, sau chuyển cho phương thức View Tôi không đặc tả view tơi gọi phương thức View, MVC Framework chọn view mặc định gắn liền với phương thức hành động (tập tin Views/Home/Index.cshtml) Liệt kê 6- Nội dung tập tin HomeController.cs using System.Web.Mvc; using System.Linq; using EssentialTools.Models; namespace EssentialTools.Controllers { public class HomeController : Controller { private Product[] products = { new Product {Name = "Kayak", Category = "Watersports", Price = 275M}, new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M}, new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M}, new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M} }; public ActionResult Index() { LinqValueCalculator calc = new LinqValueCalculator(); ShoppingCart cart = new ShoppingCart(calc) { Products = products }; 130 130 decimal totalValue = cart.CalculateProductTotal(); return View(totalValue); } } } Thêm View Bổ sung cuối cho project view , tên Index Không quan trọng bạn chọn tùy chọn tạo view, miễn bạn thiết lập nội dung giống Liệt kê 6-5 Liệt kê 6-5 Nội dung tập tin Index.cshtml @model decimal @{ Layout = null; } Value Total value is $@Model View sử dụng biểu thức @Model để hiển thị giá trị decimal chuyển từ phương thức hành động Nếu bạn khởi chạy project, bạn thấy giá trị tổng, theo tính tốn lớp LinqValueCalculator , minh họa H ìn h 6-1 Đây project bản, tạo bối cảnh cho công cụ kỹ thuật khác mà tơi mơ tả chương Hình 6- Kiểm thử ứng dụng mẫu Sử dụng Ninject Tôi giới thiệu dependency injection (DI) Chương Tóm lại, ý tưởng để tách thành phần ứng dụng MVC, với kết hợp interface DI container nhằm tạo instance đối tượng cách tạo interface mà chúng phụ thuộc vào đưa chúng vào constructor 131 131 Trong phần , Tơi giải thích vấn đề mà tơi cố tình tạo ví dụ chương cho thấy làm để sử dụng Ninject–DI container ưa thích tơi–để giải Đừng lo lắng bạn cảm thấy khơng quen với Ninject–các nguyên tắc cho tất DI container có nhiều lựa chọn thay để chọn Hiểu Vấn đề Trong ứng dụng mẫu, Tơi tạo ví dụ vấn đề mà DI phải xử lý: lớp liên kết chặt chẽ với Lớp ShoppingCart liên kết chặt chẽ với lớp LinqValueCalculator lớp HomeController liên kết chặt chẽ với ShoppingCart LinqValueCalculator Điều có nghĩa tơi muốn thay lớp LinqValueCalculator , Tơi phải xác định sau thay đổi tham chiếu lớp liên kết chặt chẽ với Đây hẳn khơng phải vấn đề với project đơn giản, trở thành trình mệt nhọc dễ bị lỗi project thực tế, đặc biệt tơi muốn chuyển đổi triển khai tính tốn khác (ví dụ, để kiểm thử), thay thay lớp với lớp khác Áp dụng Interface Tơi giải phần vấn đề cách sử dụng interface C# để trừu tượng hóa định nghĩa chức tính tốn từ triển khai Để chứng minh điều này, thêm tập tin lớp IValueCalculator.cs vào thư mục Models tạo interface Liệt kê 6-6 Liệt kê 6-6 Nội dung tập tin IValueCalculator.cs using System.Collections.Generic; namespace EssentialTools.Models { public interface IValueCalculator { decimal ValueProducts(IEnumerable products); } } Tơi sau triển khai interface lớp LinqValueCalculator , Liệt kê 6-7 Liệt kê 6-7 Áp dụng Interface tập tin LinqValueCalculator.cs using System.Collections.Generic; using System.Linq; namespace EssentialTools.Models { public class LinqValueCalculator : IValueCalculator { public decimal ValueProducts(IEnumerable products) { return products.Sum(p => p.Price); } } } Interface cho phép phá vỡ liên kết chặt chẽ lớp ShoppingCart LinqValueCalculator Liệt kê 6-8 Liệt kê 6-8 Áp dụng Interface tập tin ShoppingCart.cs using System.Collections.Generic; 132 132 namespace EssentialTools.Models { public class ShoppingCart { private IValueCalculator calc; public ShoppingCart(IValueCalculator calcParam) { calc = calcParam; } public IEnumerable Products { get; set; } public decimal CalculateProductTotal() { return calc.ValueProducts(Products); } } } Tôi thực vài cải tiến, C# yêu cầu đặc tả lớp triển khai cho interface q trình tạo instance, điều dễ hiểu cần phải biết lớp triển khai tơi muốn sử dụng–nhưng điều có nghĩa tơi vấn đề controller Home tạo đối tượng LinqValueCalculator, L iệt kê 6- Liệt kê 6-9 Áp dụng Interface cho tập tin HomeController.cs public ActionResult Index() { IValueCalculator calc = new LinqValueCalculator(); ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalculateProductTotal(); return View(totalValue); } Mục tiêu với Ninject đạt đến điểm mà xác định muốn khởi tạo triển khai interface IValueCalculator, chi tiết việc triển khai u cầu lại khơng nằm code controller Home Điều có nghĩa nói với Ninject LinqValueCalculator triển khai interface IValueCalculator mà tơi muốn sử dụng cập nhật lớp HomeController để lấy đối tượng thơng qua Ninject, thay sử dụng từ khóa new Thêm Ninject vào Visual Studio Project Cách đơn giản để thêm Ninject vào dự án MVC sử dụng hỗ trợ tích hợp Visual Studio cho NuGet, thứ giúp việc cài đặt loạt gói giữ cho chúng cập nhật trở nên dễ dàng Tôi sử dụng NuGet Chương để cài đặt thư viện Bootstrap, có danh mục lớn gói có sẵn, bao gồm Ninject Chọn Tools Library Package Manager Package Manager Console Visual Studio để mở cửa sổ dòng lệnh NuGet nhập vào lệnh sau: Install-Package Ninject -version 3.0.1.10 Install-Package Ninject.Web.Common -version 3.0.0.7 Install-Package Ninject.MVC3 -Version 3.0.0.6 Lệnh cài đặt gói cốt lõi Ninject số lại cài đặt phần mở rộng cho lõi khiến Ninject hoạt động tốt với ứng dụng ASP.NET (tôi giải thích sau đây) Đừng hiểu nhầm với phần tham chiếu MVC3 tên gói cuối cùng– hoạt động tốt với MVC 133 133 Tôi sử dụng tham số version để cài đặt phiên cụ thể gói Đây phiên viết sách Bạn nên sử dụng tham số version để đảm bảo bạn có kết ví dụ chương này, bạn bỏ qua tham số lấy phiên (và có khả gần hơn) cho project thực tế Bắt đầu với Ninject Có ba bước để làm cho chức Ninject hoạt động, bạn xem chúng Liệt kê 6-10, cho thấy thay đổi tơi thực với controller Home Mẹo Tôi từ từ phần phần Dependency Injection lúc hiểu tơi khơng muốn bỏ qua điều giúp giảm bớt bối rối Liệt kê 6-10 Thêm chức Ninject vào phương thức hành động Index tập tin HomeController.cs using System.Web.Mvc; using EssentialTools.Models; using Ninject; namespace EssentialTools.Controllers { public class HomeController : Controller { private Product[] products = { new Product {Name = "Kayak", Category = "Watersports", Price = 275M}, new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M}, new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M}, new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M} }; public ActionResult Index() { IKernel ninjectKernel = new StandardKernel(); ninjectKernel.Bind().To (); IValueCalculator calc = ninjectKernel.Get(); ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalculateProductTotal(); return View(totalValue); } } } Bước chuẩn bị Ninject để sử dụng Để làm điều này, tạo instance Ninject kernel, đối tượng có trách nhiệm giải phụ thuộc tạo đối tượng Khi cần đối tượng, sử dụng kernel thay từ khóa new Đây câu lệnh tạo kernel từ bảng liệt kê: 134 134 IKernel ninjectKernel = new StandardKernel(); Tôi cần tạo triển khai interface Ninject.IKernel, cách tạo instance lớp StandardKernel Ninject mở rộng tùy chỉnh để sử dụng loại kernel khác nhau, cần StandardKernel tích hợp sẵn chương (Trên thực tế, sử dụng Ninject nhiều năm cần StandardKernel) Bước thứ hai trình cấu hình kernel Ninject để hiểu đối tượng triển khai mà muốn sử dụng cho interface mà làm việc với Đây câu lệnh từ bảng liệt kê mà làm việc đó: ninjectKernel.Bind< IValueCalculator >().To< LinqValueCalculator >(); Ninject sử dụng tham số kiểu C # để tạo mối quan hệ: thiết lập interface mà muốn làm việc với tham số kiểu cho phương thức Bind gọi phương thức To kết trả Tơi thiết lập lớp triển khai muốn khởi tạo tham số kiểu cho phương thức To Câu lệnh nói với Ninject phụ thuộc interface IValueCalculator nên giải cách tạo instance lớp LinqValueCalculator Bước cuối sử dụng Ninject để tạo đối tượng, thông qua phương thức Get kernel, này: IValueCalculator calc = ninjectKernel.Get() ; Các tham số kiểu sử dụng cho phương thức Get nói với Ninject interface mà tơi quan tâm kết từ phương thức instance kiểu triển khai định với phương thức To lúc Thiết lập MVC Dependency Injection Kết ba bước giới thiệu phần trước kiến thức lớp triển khai cần khởi tạo nhằm đáp ứng yêu cầu cho interface IValueCalculator vốn thiết lập Ninject Tất nhiên, chưa cải thiện ứng dụng kiến thức xác định controller Home, có nghĩa controller Home liên kết chặt chẽ với lớp LinqValueCalculator Trong phần sau, cho bạn cách để nhúng Ninject vào trung tâm ứng dụng MVC, điều cho phép tơi đơn giản hóa controller, mở rộng ảnh hưởng Ninject để hoạt động khắp ứng dụng, di chuyển cấu hình khỏi controller Tạo Dependency Resolver Thay đổi làm tạo custom dependency resolver MVC Framework sử dụng dependency resolver để tạo instance lớp mà cần phải phục vụ yêu cầu Bằng cách tạo custom resolver, tơi đảm bảo MVC Framework sử dụng Ninject tạo đối tượng–bao gồm instance controller, chẳng hạn Để thiết lập resolver, Tơi tạo thư mục có tên Infrastructure , thư mục mà tơi sử dụng để bỏ lớp mà không phù hợp với thư mục khác ứng dụng MVC Tôi thêm tập tin lớp vào thư mục có tên NinjectDependencyResolver.cs, nội dung bạn xem Liệt kê 6-11 Liệt kê 6-11 Nội dung tập tin NinjectDependencyResolver.cs using using using using using System; System.Collections.Generic; System.Web.Mvc; EssentialTools.Models; Ninject; 135 135 namespace EssentialTools.Infrastructure { public class NinjectDependencyResolver : IDependencyResolver { private IKernel kernel; public NinjectDependencyResolver(IKernel kernelParam) { kernel = kernelParam; AddBindings(); } public object GetService(Type serviceType) { return kernel.TryGet(serviceType); } public IEnumerable GetServices(Type serviceType) { return kernel.GetAll(serviceType); } private void AddBindings() { kernel.Bind().To(); } } } Lớp NinjectDependencyResolver triển khai interface IDependencyResolver, phần namespace System.Mvc mà MVC Framework sử dụng để lấy đối tượng cần MVC Framework gọi phương thức GetService GetServices cần instance lớp để phục vụ yêu cầu vào Công việc dependency resolver tạo instance đó, nhiệm vụ mà thực cách gọi phương thức TryGet GetAll Ninject Phương thức TryGet hoạt động phương thức Get tơi sử dụng trước đó, trả null khơng có ràng buộc phù hợp thay exception Phương thức GetAll hỗ trợ nhiều ràng buộc cho kiểu nhất, sử dụng có số đối tượng triển khai khác hữu Lớp dependency resolver nơi mà thiết lập ràng buộc Ninject Trong phương thức AddBindings, sử dụng phương thức Bind To để cấu hình mối quan hệ interface IValueCalculator lớp LinqValueCalculator Đăng ký cho Dependency Resolver Nếu đơn tạo triển khai cho interface IDependencyResolver thơi chưa đủ–Tơi phải nói với MVC Framework tơi muốn sử dụng Các gói Ninject tơi thêm vào NuGet tạo tập tin có tên NinjectWebCommon.cs thư mục App_Start định nghĩa phương thức gọi cách tự động ứng dụng khởi chạy, để tích hợp vào vòng đời yêu cầu ASP.NET (Điều để cung cấp tính phạm vi mà tơi mơ tả sau chương này) Trong phương thức RegisterServices lớp NinjectWebCommon, thêm câu lệnh để tạo instance lớp NinjectDependencyResolver sử dụng phương thức static SetResolver định nghĩa lớp System.Web.Mvc.DependencyResolver để đăng ký resolver với MVC Framework, L iệ t kê 6- Đừng lo lắng cảm thấy điều không hợp lý Tác dụng câu lệnh để tạo cầu nối Ninject hỗ trợ MVC Framework cho DI Liệt kê 6-12 Đăng ký Resolver tập tin NinjectWebCommon.cs private static void RegisterServices(IKernel kernel) { System.Web.Mvc.DependencyResolver.SetResolver(new EssentialTools.Infrastructure.NinjectDependencyResolver(kernel)); } 136 136 Tái cấu trúc lại Controller Home Bước cuối phải tái cấu trúc lại controller Home để có lợi phương tiện mà thiết lập phần trước Bạn thấy thay đổi mà thực Liệt kê 6-13 Liệt kê 6-13 Tái cấu trúc lại Controller tập tin HomeController.cs using System.Web.Mvc; using EssentialTools.Models; namespace EssentialTools.Controllers { public class HomeController : Controller { private IValueCalculator calc; private Product[] products = { new Product {Name = "Kayak", Category = "Watersports", Price = 275M}, new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M}, new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M}, new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M} }; public HomeController(IValueCalculator calcParam) { calc = calcParam; } public ActionResult Index() { ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalculateProductTotal(); return View(totalValue); } } } Thay đổi mà tơi làm thêm constructor lớp chấp nhận triển khai interface IValueCalculator, thay đổi lớp HomeController để khai báo phụ thuộc Ninject cung cấp đối tượng nhằm triển khai interface IValueCalculator tạo instance controller, sử dụng cấu hình mà tơi thiết lập lớp NinjectDependencyResolver L iệt k ê 6- Thay đổi khác mà làm bỏ đề cập Ninject lớp LinqValueCalculator từ controller Sau cùng, phá vỡ liên kết chặt chẽ HomeController lớp LinqValueCalculator Nếu bạn chạy ví dụ này, bạn thấy kết Hình 6-2 Tất nhiên, tơi có kết tương tự khởi tạo lớp LinqValueCalculator trực tiếp controller 137 137 Hình 6-5 Thêm Tham chiếu tới project MVC Tạo Các Kiểm thử Đơn vị Tôi thêm kiểm thử đơn vị vào tập tin UnitTest1.cs project EssentialTools.Tests Bản có phí Visual Studio có vài tính hay giúp tự động phát sinh phương thức kiểm thử cho lớp chúng khơng có sẵn Express, tơi tạo kiểm thử hữu ích có ý nghĩa Để bắt đầu, tơi thực thay đổi thể Liệt kê 6-27 Liệt kê 6-27 Thêm Phương thức Kiểm thử tập tin UnitTest1.cs using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using EssentialTools.Models; namespace EssentialTools.Tests { [TestClass] public class UnitTest1 { private IDiscountHelper getTestObject() { return new MinimumDiscountHelper(); } [TestMethod] public void Discount_Above_100() { // arrange IDiscountHelper target = getTestObject(); decimal total = 200; // act var discountedTotal = target.ApplyDiscount(total); 147 147 // assert Assert.AreEqual(total * 0.9M, discountedTotal); } } } Tôi thêm kiểm thử đơn vị Một lớp có chứa kiểm thử thích với thuộc tính TestClass kiểm thử đơn lẻ phương thức thích với thuộc tính TestMethod Khơng phải tất phương thức lớp kiểm thử đơn vị phải kiểm thử đơn vị Để chứng minh điều này, định nghĩa phương thức getTestObject, mà sử dụng để xếp kiểm thử tơi Bởi phương thức khơng có thuộc tính TestMethod, Visual Studio khơng xem kiểm thử đơn vị Mẹo Chú ý tơi có thêm câu lệnh using để nhập namespace EssentialTools.Models vào lớp kiểm thử Các lớp kiểm thử lớp C# bình thường khơng có kiến thức đặc biệt project MVC Các thuộc tính TestClass TestMethod thứ mang đến khả kiểm thử cho project Bạn thấy tơi làm theo mơ hình bố trí/thực thi/xác nhận (A/A/A) phương thức kiểm thử đơn vị mà mô tả C h n g Có vơ số quy ước cách đặt tên cho kiểm thử đơn vị, lời khuyên đơn giản bạn sử dụng tên cho bật thứ mà kiểm thử kiểm tra Phương thức kiểm thử đơn vị tơi tên Discount_Above_100, rõ ràng có nghĩa với tơi Nhưng quan trọng bạn (và nhóm bạn) hiểu mơ típ đặt tên mà bạn sử dụng, bạn áp dụng kiểu đặt tên khác bạn khơng thích cách tơi Tơi thiết lập phương thức kiểm thử cách gọi phương thức getTestObject, qua tạo instance đối tượng kiểm thử: trường hợp lớp MinimumDiscountHelper Tôi định nghĩa giá trị total mà kiểm thử Đây phần bố trí kiểm thử đơn vị Đối với phần thực thi kiểm thử, gọi phương thức MinimumDiscountHelper.ApplyDiscount gán kết cho biến discountedTotal Cuối cùng, phần xác nhận kiểm thử, sử dụng phương thức Assert.AreEqual để kiểm tra giá trị mà tơi có từ phương thức ApplyDiscount có 90% tổng giá trị lúc đầu Lớp Assert có loạt phương thức static mà bạn sử dụng kiểm thử Lớp namespace Microsoft.VisualStudio.TestTools.UnitTesting với số lớp có ích cho việc thiết lập thực kiểm thử Bạn tìm hiểu thêm lớp namespace http://msdn.microsoft.com/en -us/library/ms182530.aspx Lớp Assert lớp mà sử dụng nhiều tóm tắt phương pháp quan trọng B ả n g - Bảng 6-4 Các phương thức xác nhận static Phương thức Mô tả AreEqual(T, T) AreEqual(T, T, string) Xác nhận đối tượng kiểu T có giá trị AreNotEqual(T, T) AreNotEqual(T, T, string) Xác nhận đối tượng kiểu T khơng có giá trị AreSame(T, T) AreSame(T, T, string) Xác nhận biến tham chiếu đến đối tượng AreNotSame(T, T) AreNotSame(T, T, string) Xác nhận biến tham chiếu đến đối tượng khác Fail() Fail(string) Hủy xác nhận: khơng có điều kiện kiểm tra Inconclusive() Inconclusive(string) Cho thấy kết kiểm thử đơn vị lập cách rõ ràng IsTrue(bool) IsTrue(bool, string) Xác nhận giá trị bool t rue Thường dùng để đánh giá biểu thức trả kết bool IsFalse(bool) IsFalse(bool, string) Xác nhận giá trị IsNull(object) IsNull(object, string) Xác nhận biến không gán đối tượng tham chiếu IsNotNull(object) IsNotNull(object, string) Xác nhận biến gán đối tượng tham chiếu bool false IsInstanceOfType(object, Type) IsInstanceOfType(object, Type, string) Xác nhận đối tượng thuộc kiểu cụ thể có nguồn gốc từ kiểu cụ thể IsNotInstanceOfType(object, Type) IsNotInstanceOfType(object, Type, string) Xác nhận đối tượng không thuộc kiểu cụ thể 148 148 Mỗi phương thức static lớp Assert cho phép bạn kiểm tra số khía cạnh kiểm thử đơn vị phương thức cho ngoại lệ kiểm tra thất bại Tất xác nhận phải thành cơng kiểm thử đơn vị coi đạt Mỗi phương thức bảng có phiên tải nhận than số string String thêm vào yếu tố thông báo ngoại lệ việc xác nhận thất bại Phương thức AreEqual AreNotEqual có số tải phục vụ cho việc so sánh kiểu cụ thể Ví dụ, có phiên cho phép chuỗi so sánh mà không cần động đến tài khoản Mẹo Một thứ đáng ý namespace Microsoft.VisualStudio.TestTools.UnitTesting thuộc tính ExpectedException Đây xác nhận mà thành công kiểm thử đơn vị cho ngoại lệ thuộc kiểu quy định tham số ExceptionType Đây cách đơn giản để bảo đảm ngoại lệ tạo mà không cần phải động tới khối try catch kiểm thử đơn vị bạn Hiện giới thiệu cho bạn cách tạo kiểm thử đơn vị, Tôi thêm kiểm thử chuyên sâu để kiểm thử project để xác nhận hành vi khác mà muốn cho lớp MinimumDiscountHelper Bạn thấy bổ sung Liệt kê 6-28, kiểm thử đơn vị ngắn đơn giản (đây nói chung đặc tính kiểm thử đơn vị) nên tơi khơng giải thích chúng cách chi tiết Liệt kê 6-28 Định nghĩa Các Kiểm thử lại tập tin UnitTest1.cs using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using EssentialTools.Models; namespace EssentialTools.Tests { [TestClass] public class UnitTest1 { private IDiscountHelper getTestObject() { return new MinimumDiscountHelper(); } [TestMethod] public void Discount_Above_100() { // arrange IDiscountHelper target = getTestObject(); decimal total = 200; // act var discountedTotal = target.ApplyDiscount(total); // assert Assert.AreEqual(total * 0.9M, discountedTotal); } [TestMethod] public void Discount_Between_10_And_100() { //arrange IDiscountHelper target = getTestObject(); // act decimal TenDollarDiscount = target.ApplyDiscount(10); decimal HundredDollarDiscount = target.ApplyDiscount(100); decimal FiftyDollarDiscount = target.ApplyDiscount(50); // assert 149 149 Assert.AreEqual(5, TenDollarDiscount, "$10 discount is wrong"); Assert.AreEqual(95, HundredDollarDiscount, "$100 discount is wrong"); Assert.AreEqual(45, FiftyDollarDiscount, "$50 discount is wrong"); } [TestMethod] public void Discount_Less_Than_10() { //arrange IDiscountHelper target = getTestObject(); // act decimal discount5 = target.ApplyDiscount(5); decimal discount0 = target.ApplyDiscount(0); // assert Assert.AreEqual(5, discount5); Assert.AreEqual(0, discount0); } [TestMethod] [ExpectedException(typeof(ArgumentOutOfRangeException))] public void Discount_Negative_Total() { //arrange IDiscountHelper target = getTestObject(); // act target.ApplyDiscount(-1); } } } Chạy Kiểm thử Đơn vị (và Thất bại) Visual Studio cung cấp cửa sổ Test Explorer để quản lý chạy kiểm thử Chọn Windows TestExplorer từ menu Test Visual Studio để thấy cửa sổ nhấn nút Run All gần góc trên, bên trái Bạn thấy kết giống Hình 6-6 150 150 Hình 6-6 Chạy kiểm thử project Bạn thấy danh sách kiểm thử tơi định nghĩatrong bảng điều khiển bên tay trái cửa sổ Test Explorer Tất nhiên, tất kiểm thử thất bại, chưa triển khai phương thức mà tơi kiểm thử Bạn nhấn vào kiểm thử cửa sổ để xem lý thất bại Cửa sổ Test Explorer cung cấp hàng loạt cách khác để chọn lọc kiểm thử đơn vị chọn kiểm thử để chạy Tuy nhiên, project mẫu đơn giản tôi, cho chạy tất kiểm thử cách nhấn Run All Triển khai Tính Đã đến lúc tơi triển khai tính năng, tự tin tơi kiểm tra code hoạt động mong đợi tơi hồn thành Với tất chuẩn bị tôi, việc triển khai lớp MinimumDiscountHelper đơn giản, Liệt kê 629 Liệt kê 6-29 Nội dung tập tin MinimumDiscountHelper.cs using System; namespace EssentialTools.Models { public class MinimumDiscountHelper : IDiscountHelper { public decimal ApplyDiscount(decimal totalParam) { if (totalParam < 0) { throw new ArgumentOutOfRangeException(); } else if (totalParam > 100) { return totalParam * 0.9M; } else if (totalParam > 10 && totalParam 10 && totalParam < 100) { Đặc tả mà triển khai đề hành vi cho giá trị bao hàm $10 $100, triển khai lại bỏ qua hai giá trị kiểm tra giá trị lớn $10, bỏ qua tổng $10 Cách giải đơn giản thể Liệt kê 6-30 Chỉ cần bổ sung thêm ký tự thay đổi hiệu ứng câu lệnh if Liệt kê 6-30 Sửa Code Tính tập tin MinimumDiscountHelper.cs using System; namespace EssentialTools.Models { public class MinimumDiscountHelper : IDiscountHelper { 152 152 public decimal ApplyDiscount(decimal totalParam) { if (totalParam < 0) { throw new ArgumentOutOfRangeException(); } else if (totalParam > 100) { return totalParam * 0.9M; } else if (totalParam >= 10 && totalParam p.Price)); } } } Để kiểm thử lớp này, thêm lớp kiểm thử đơn vị vào project kiểm thử Bạn làm điều cách kích chuột phải vào project kiểm thử Solution Explorer chọn Add Unit Test từ menu Nếu menu Add bạn khơng có item Unit Test, chọn New Item sử dụng template Basic Unit Test Bạn thấy thay đổi thực tập tin mới, mà Visual Studio mặc định đặt tên UnitTest2.cs, Liệt kê 6-32 Liệt kê 6-32 Thêm Kiểm thử Đơn vị cho lớp ShoppingCart tập tin UnitTest2.cs using using using using System; Microsoft.VisualStudio.TestTools.UnitTesting; EssentialTools.Models; System.Linq; namespace EssentialTools.Tests { [TestClass] public class UnitTest2 { private Product[] products = { new Product {Name = "Kayak", Category = "Watersports", Price = 275M}, new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M}, new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M}, new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M} }; [TestMethod] public void Sum_Products_Correctly() { // arrange var discounter = new MinimumDiscountHelper(); var target = new LinqValueCalculator(discounter); 154 154 var goalTotal = products.Sum(e => e.Price); // act var result = target.ValueProducts(products); // assert Assert.AreEqual(goalTotal, result); } } } Vấn đề mà đối mặt lớp LinqValueCalculator phụ thuộc vào triển khai interface IDiscountHelper để hoạt động Trong ví dụ này, dùng lớp MinimumDiscountHelper điều làm phát sinh hai vấn đề Thứ nhất, khiến kiểm thử đơn vị phức tạp dễ vỡ Để tạo kiểm thử đơn vị hoạt động, cần xem xét logic giảm giá triển khai IDiscountHelper để tìm giá trị dự kiến từ phương thức ValueProducts Yếu tố dễ vỡ bắt nguốn từ thực tế kiểm thử tơi thất bại logic giảm giá triển khai thay đổi, lớp LinqValueCalculator vận hành cách Thứ hai, rắc rối nhất, mở rộng phạm vi kiểm thử đơn vị vơ tình bao gồm lớp MinimumDiscountHelper Khi điểm thử đơn vị thất bại, lớp LinqValueCalculator hay MinimumDiscountHelper có vấn đề Các kiểm thử đơn vị tốt nên đơn giản tập trung, thiết lập không sở hữu hai đặc điểm Trong phần sau , cho bạn cách thêm áp dụng Moq project MVC để bạn tránh vấn đề Thêm Moq vào Project Visual Studio Cũng giống với Ninject hồi đầu chương, cách dễ để thêm Moq vào project MVC sử dụng hỗ trợ tích hợp Visual Studio cho NuGet Mở giao diện điều khiển NuGet gõ dòng lệnh sau: Install-Package Moq EssentialTools.Tests -version 4.1.1309.1617 -projectname Tham số projectname cho phép tơi nói với NuGet tơi muốn gói Moq cài đặt project kiểm thử đơn vị tơi, thay ứng dụng Thêm Đối tượng Giả vào Kiểm thử Đơn vị Thêm đối tượng giả vào kiểm thử đơn vị tức nói với Moq loại đối tượng bạn muốn làm việc với, cấu hình hành vi sau áp dụng đối tượng cho mục tiêu kiểm thử Bạn thấy cách tơi thêm đối tượng giả vào kiểm thử đơn vị cho LinqValueCalculator L iệt kê 6- 33 Liệt kê 6-33 Sử dụng Đối tượng Giả Kiểm thử Đơn vị tập tin UnitTest2.cs using using using using EssentialTools.Models; Microsoft.VisualStudio.TestTools.UnitTesting; System.Linq; Moq; namespace EssentialTools.Tests { [TestClass] public class UnitTest2 { private Product[] products = { new Product {Name = "Kayak", Category = "Watersports", Price = 275M}, new Product {Name = "Lifejacket", Category = "Watersports", 155 155 Price = 48.95M}, new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M}, new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M} }; [TestMethod] public void Sum_Products_Correctly() { // arrange Mock mock = new Mock(); mock.Setup(m => m.ApplyDiscount(It.IsAny())) Returns(total => total); var target = new LinqValueCalculator(mock.Object); // act var result = target.ValueProducts(products); // assert Assert.AreEqual(products.Sum(e => e.Price), result); } } } Cú pháp để sử dụng Moq kỳ lạ bạn lần nhìn thấy nó, tơi qua giai đoạn q trình Mẹo Lưu ý có số thư viện làm giả khác sẵn có, bạn tìm cho tháy phù hợp bạn khơng thích cách Moq làm việc, Moq thư viện dễ sử dụng Bạn mong đợi số thư viện phổ biến khác có hướng dẫn sử dụng dài hàng trăm trang Tạo Đối tượng Giả Bước nói với Moq loại đối tượng giả mà bạn muốn làm việc với Moq chủ yếu dựa vào tham số kiểu, bạn thấy điều qua cách mà tơi nói với Moq tơi muốn tạo triển khai IDiscountHelper giả: Mock mock = new Mock(); Tôi tạo đối tượng strongly typed Mock, nói với thư viện Moq kiểu mà xử lý Đây interface IDiscountHelper cho kiểm thử đơn vị tơi, kiểu mà bạn muốn cô lập để cải thiện độ tập trung kiểm thử đơn vị bạn Lựa chọn Phương thức Ngoài việc tạo đối tượng strongly typed Mock, Tôi cần phải đặc tả cách mà hành xử Việc trung tâm q trình làm giả cho phép bạn đảm bảo bạn thiết lập hành vi đối tượng giả, mà bạn sử dụng để kiểm thử chức đối tượng mục tiêu bạn kiểm thử đơn vị Đây câu lệnh từ kiểm thử đơn vị thiết lập nên hành vi mà muốn: mock.Setup (m => (total => total); m.ApplyDiscount(It.IsAny())).Returns Tôi dùng phương thức Setup để thêm phương thức vào đối tượng giả Moq làm việc biểu thức LINQ 156 156 lambda Khi gọi phương thức Setup, Moq chuyển cho tơi interface mà tơi u cầu triển khai, khéo léo gói gọn LINQ mà tơi không đào sâu vào chi tiết Điều cho phép tơi chọn phương thức tơi muốn cấu hình cách sử dụng biểu thức lambda Đối với kiểm thử đơn vị tôi, Tôi muốn định nghĩ hành vi phương thức ApplyDiscount, phương thức interface IDiscountHelper, phương thức cần để kiểm thử lớp LinqValueCalculator Tôi cần phải nói vơi Moq tham số giá trị mà tơi quan tâm, cách sử dụng lớp It , mà đánh dấu sau: mock.Setup(m => (total => total); m.ApplyDiscount(It.IsAny())).Returns Lớp It định nghĩa số phương thức sử dụng với thông số kiểu tổng quát Trong trường hợp này, Tôi gọi phương thức IsAny sử dụng decimal kiểu tổng quát Điều bảo Moq áp dụng hành vi mà định nghĩa gọi phương thức ApplyDiscount cho giá trị thập phân B ảng 6- cho thấy phương thức mà lớp It cung cấp, chúng static Bảng 6-5 Các phương thức lớp It Phương thức Mô tả Is(predicate) Xác định giá trị thuộc kiểu IsAny() Xác định giá trị thuộc kiểu IsInRange(min, Phù hợp tham số nằm giá trị định nghĩa thuộc kiểu max, kind) IsRegex(expr) T mà vị từ trả true Xem ví dụ Liệt kê 6-34) T T Tham số cuối giá trị thuộc kiểu liệt kê Range Bao Hàm Không Bao Hàm Phù hợp với tham số chuỗi phù hợp với biểu thức thông thường định Tôi cho bạn thấy ví dụ phức tạp sau mà sử dụng số phương thức It khác, lúc tơi nói phương thức IsAny mà cho phép phản hồi giá trị thập phân Đặc tả Kết Phương thức Returns cho phép đặc tả kết mà Moq trả gọi phương thức làm giả Tơi đặc tả kiểu kết tham số kiểuvà đặc tả thân kết biểu thức lambda Bạn xem cách tơi làm điều cho ví dụ: mock.Setup(m => (total => total) ; m.ApplyDiscount(It.IsAny())).Returns Bằng cách gọi phương thức Returns với tham số kiểu decimal (v.d., Returns ), tơi nói với Moq tơi trả giá trị decimal Đối với biểu thức lambda, Moq chuyển cho giá trị thuộc kiểu nhận phương thức ApplyDiscount Tôi tạo phương thức pass-through ví dụ, mà tơi trả giá trị truyền từ phương thức giả ApplyDiscount mà không thực thao tác Đây loại phương pháp giả đơn giản nhất, tơi sớm cho bạn thấy ví dụ phức tạp Sử dụng Đối tượng Giả Bước cuối sử sụng đối tượng giả kiểm thử đơn vị, mà thực cách đọc giá trị thuộc tính Object đối tượng Mock: var target = new LinqValueCalculator(mock.Object); 157 157 Nói cách tóm tắt, thuộc tính Object trả triển khai interfce IDiscountHelper nơi mà phương thức ApplyDiscount trả giá trị tham số decimal truyền Điều giúp kiểm thử đơn vị dễ thực tơi tự cộng giá đối tượng Product kiểm thử kiểm tra xem tơi có nhận giá trị trả từ đối tượng LinqValueCalculator: Assert.AreEqual(products.Sum(e => e.Price), result); Lợi ích việc sử dụng Moq theo cách kiểm thử đơn vị kiểm tra hành vi đối tượng LinqValueCalculator không phụ thuộc vào triển khai thực tế interface IDiscountHelper thư mục Models Điều có nghĩa kiểm thử thất bại, biết vấn đề nằm triển khai LinqValueCalculator cách mà thiết lập đối tượng giả, giải vấn đề từ nguồn đơn giản dễ dàng đối phó với chuỗi đối tượng thực tế tương tác chúng Tạo Đối tượng Giả phức tạp Tôi cho bạn thấy đối tượng giả đơn giản phần trước, điểm mạnh Moq khả xây dựng hành vi phức tạp để kiểm thử tình khác Trong Liệt kê 6-34, Tôi thêm kiểm thử đơn vị vào tập tin UnitTest2.cs mà làm giả triển khai phức tạp interface IDiscountHelper Thực ra, tơi sử dụng Moq để mơ hình hóa hành vi lớp MinimumDiscountHelper Liệt kê 6-34 Làm giả hành vi lớp MinimumDiscountHelper tập tin UnitTest2.cs using using using using EssentialTools.Models; Microsoft.VisualStudio.TestTools.UnitTesting; Moq; System.Linq; namespace EssentialTools.Tests { [TestClass] public class UnitTest2 { private Product[] products = { new Product {Name = "Kayak", Category = "Watersports", Price = 275M}, new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M}, new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M}, new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M} }; [TestMethod] public void Sum_Products_Correctly() { // arrange Mock mock = new Mock(); mock.Setup(m => m.ApplyDiscount(It.IsAny())) Returns(total => total); var target = new LinqValueCalculator(mock.Object); // act var result = target.ValueProducts(products); // assert 158 158 Assert.AreEqual(products.Sum(e => e.Price), result); } private Product[] createProduct(decimal value) { return new[] { new Product { Price = value } }; } [TestMethod] [ExpectedException(typeof(System.ArgumentOutOfRangeException))] public void Pass_Through_Variable_Discounts() { // arrange Mock mock = new Mock(); mock.Setup(m => m.ApplyDiscount(It.IsAny())) Returns(total => total); mock.Setup(m => m.ApplyDiscount(It.Is(v => v == 0))) Throws(); mock.Setup(m => m.ApplyDiscount(It.Is(v => v > 100))) Returns(total => (total * 0.9M)); mock.Setup(m => m.ApplyDiscount(It.IsInRange(10, 100, Range.Inclusive))).Returns(total => total - 5); var target = new LinqValueCalculator(mock.Object); // act decimal FiveDollarDiscount = target.ValueProducts(createProduct(5)); decimal TenDollarDiscount = target.ValueProducts(createProduct(10)); decimal FiftyDollarDiscount = target.ValueProducts(createProduct(50)); decimal HundredDollarDiscount target.ValueProducts(createProduct(100)); decimal FiveHundredDollarDiscount target.ValueProducts(createProduct(500)); = = // assert Assert.AreEqual(5, FiveDollarDiscount, "$5 Fail"); Assert.AreEqual(5, TenDollarDiscount, "$10 Fail"); Assert.AreEqual(45, FiftyDollarDiscount, "$50 Fail"); Assert.AreEqual(95, HundredDollarDiscount, "$100 Fail"); Assert.AreEqual(450, FiveHundredDollarDiscount, "$500 Fail"); target.ValueProducts(createProduct(0)); } } } Xét kiểm thử đơn vị, chép hành vi dự kiến lớp model khác việc làm kỳ lạ, minh họa hồn hảo số tính khác Moq Tơi định nghĩa bốn hành vi khác cho phương thức ApplyDiscount dựa giá trị tham số mà nhận Cách đơn giản nhận tất cả, trả giá trị cho giá trị decimal nào, này: mock.Setup(m => (total => total); m.ApplyDiscount(It.IsAny())).Returns 159 159 Đây hành vi tương tự sử dụng ví dụ trước, tơi bao gồm thứ tự mà bạn gọi phương thức Setup ảnh hưởng đến hành vi đối tượng giả Moq đánh giá hành vi theo thứ tự ngược, xem xét lời gọi gần tới phương thức Setup Điều có nghĩa bạn phải tạo hành vi giả bạn theo thứ tự từ tổng quát đến cụ thể Điều kiện It.IsAny điều kiện tổng quát mà định nghĩa ví dụ tơi áp dụng trước tiên Nếu tơi đảo ngược thứ tự lời gọi Setup , hành vi bắt tất lời gọi thực tới phương thức ApplyDiscount tạo kết giả sai Làm giả Đối với Các giá trị Cụ thể (và Tạo Ngoại lệ) Đối với lời gọi thứ hai tới phương thức Setup, sử dụng phương thức It.Is: mock.Setup(m => m.ApplyDiscount(It.Is(v => v == 0))) Throws(); Vị từ mà truyền cho phương thức Is trả true giá trị truyền cho phương thức ApplyDiscount Thay trả kết quả, tơi dùng phương thức Throws, khiến Moq tạo instance ngoại lệ đặc tả với tham số kiểu Tôi dùng phương thức Is để bắt giá trị lớn 100, này: mock.Setup(m => m.ApplyDiscount(It.Is(v => v > 100))) Returns(total => (total * 0.9M)); Phương thức It.Is cách linh hoạt cho việc thiết lập hành vi cụ thể cho giá trị tham số khác bạn dùng vị từ trả true false Đây phương thức thường sử dụng tạo đối tượng giả phức tạp Làm giả Đối với Dãy Các giá trị Cuối cùng, sử dụng đối tượng It với phương thức IsInRange, cho phép bắt dãy giá trị tham số: mock.Setup(m => m.ApplyDiscount(It.IsInRange(10, Range.Inclusive))) Returns(total => total - 5); 100, Tôi bao gồm điều cho đầy đủ, project mình, tơi thường sử dụng phương thức Is vị từ để làm điều tương tự, này: mock.Setup(m => m.ApplyDiscount(It.Is(v => v >= 10 && v total - 5); Hiệu nhau, thấy phương pháp vị từ linh hoạt Moq có loạt tính hữu ích bạn xem làm để áp dụng chúng cách đọc hướng dẫn nhanh cung cấp : http://code.google.com/p/moq/wiki/QuickStart Tổng kết Trong chương này, xem xét công cụ mà thấy cần thiết cho việc phát triển MVC hiệu quả: Ninject, hỗ trợ kiểm thử đơn vị tích hợp Visual Studio, Moq Có nhiều giải pháp thay thế, mã nguồn mở lẫn thương mại, cho ba công cụ 160 160 bạn không thiếu giải pháp thay bạn không quen với công cụ mà tơi thích sử dụng Bạn thấy nhìn chung bạn khơng thích TDD kiểm tra đơn vị, bạn thích thực DI làm giả cách thủ cơng Điều đó, tất nhiên, hồn tồn chọn lựa bạn Tuy nhiên, Tơi nghĩ có số lợi ích đáng kể sử dụng ba công cụ chu kỳ phát triển Nếu bạn dự áp dụng chúng bạn chưa thử chúng, Tơi khun bạn tạm thời bn bỏ hồi nghi cho chúng hội, khn khổ sách 161 161

Ngày đăng: 23/10/2019, 21:16

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan