Beberapa tahun lalu saya menulis tentang perkara ini, tetapi kurang terperinci. Berikut ialah versi idea yang sama yang lebih halus.
Ujian unit adalah kebaikan dan keburukan kepada pembangun. Mereka membenarkan ujian pantas kefungsian, contoh penggunaan yang boleh dibaca, percubaan pantas senario untuk komponen yang terlibat sahaja. Tetapi ia juga boleh menjadi kucar-kacir, memerlukan penyelenggaraan dan kemas kini dengan setiap perubahan kod dan, apabila dilakukan dengan malas, tidak boleh menyembunyikan pepijat daripada mendedahkannya.
Saya rasa sebab ujian unit begitu sukar adalah kerana ia dikaitkan dengan ujian, sesuatu selain daripada penulisan kod, dan juga ujian unit ditulis dengan cara yang bertentangan dengan kebanyakan kod lain yang kami tulis.
Dalam siaran ini, saya akan memberi anda corak mudah menulis ujian unit yang akan meningkatkan semua faedah, sambil menghapuskan kebanyakan disonans kognitif dengan kod biasa. Ujian unit akan kekal boleh dibaca dan fleksibel, sambil mengurangkan kod pendua dan tidak menambah kebergantungan tambahan.
Tetapi pertama sekali, mari kita tentukan suite ujian unit yang baik.
Untuk menguji kelas dengan betul, ia perlu ditulis dengan cara tertentu. Dalam siaran ini, kami akan merangkumi kelas menggunakan suntikan pembina untuk kebergantungan, yang merupakan cara saya yang disyorkan untuk melakukan suntikan kebergantungan.
Kemudian, untuk mengujinya, kita perlu:
Tetapi itu lebih mudah diucapkan daripada dilakukan, kerana ia juga membayangkan:
Siapa suka itu?
Penyelesaian adalah dengan menggunakan corak perisian pembina untuk mencipta ujian yang cair, fleksibel dan boleh dibaca dalam struktur Susun-Bertindak-Tegas, sambil merangkum kod persediaan dalam kelas yang melengkapkan suite ujian unit untuk perkhidmatan tertentu. Saya memanggil ini corak MockManager.
Mari kita mulakan dengan contoh mudah:
// the tested class public class Calculator { private readonly ITokenParser tokenParser; private readonly IMathOperationFactory operationFactory; private readonly ICache cache; private readonly ILogger logger; public Calculator( ITokenParser tokenParser, IMathOperationFactory operationFactory, ICache cache, ILogger logger) { this.tokenParser = tokenParser; this.operationFactory = operationFactory; this.cache = cache; this.logger = logger; } public int Calculate(string input) { var result = cache.Get(input); if (result.HasValue) { logger.LogInformation("from cache"); return result.Value; } var tokens = tokenParser.Parse(input); IOperation operation = null; foreach(var token in tokens) { if (operation is null) { operation = operationFactory.GetOperation(token.OperationType); continue; } if (result is null) { result = token.Value; continue; } else { if (result is null) { throw new InvalidOperationException("Could not calculate result"); } result = operation.Execute(result.Value, token.Value); operation = null; } } cache.Set(input, result.Value); logger.LogInformation("from operation"); return result.Value; } }
Ini adalah kalkulator, seperti tradisi. Ia menerima rentetan dan mengembalikan nilai integer. Ia juga menyimpan hasil carian untuk input tertentu, dan log beberapa perkara. Operasi sebenar sedang diabstrak oleh IMathOperationFactory dan rentetan input diterjemahkan ke dalam token oleh ITokenParser. Jangan risau, ini bukan kelas sebenar, hanya contoh. Mari lihat ujian "tradisional":
[TestMethod] public void Calculate_AdditionWorks() { // Arrange var tokenParserMock = new Mock<ITokenParser>(); tokenParserMock .Setup(m => m.Parse(It.IsAny<string>())) .Returns( new List<CalculatorToken> { CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1) } ); var mathOperationFactoryMock = new Mock<IMathOperationFactory>(); var operationMock = new Mock<IOperation>(); operationMock .Setup(m => m.Execute(1, 1)) .Returns(2); mathOperationFactoryMock .Setup(m => m.GetOperation(OperationType.Add)) .Returns(operationMock.Object); var cacheMock = new Mock<ICache>(); var loggerMock = new Mock<ILogger>(); var service = new Calculator( tokenParserMock.Object, mathOperationFactoryMock.Object, cacheMock.Object, loggerMock.Object); // Act service.Calculate(""); //Assert mathOperationFactoryMock .Verify(m => m.GetOperation(OperationType.Add), Times.Once); operationMock .Verify(m => m.Execute(1, 1), Times.Once); }
Jom bongkar sikit. Kami terpaksa mengisytiharkan olok-olok untuk setiap pergantungan pembina, walaupun kami sebenarnya tidak mengambil berat tentang pembalak atau cache, sebagai contoh. Kami juga terpaksa menyediakan kaedah olok-olok yang mengembalikan olok-olok lain, dalam kes kilang operasi.
Dalam ujian khusus ini, kebanyakannya kami menulis persediaan, satu baris Act dan dua baris Assert. Lebih-lebih lagi, jika kami ingin menguji cara cache berfungsi di dalam kelas, kami perlu menyalin tampal keseluruhannya dan hanya mengubah cara kami menyediakan mock cache.
Dan terdapat ujian negatif untuk dipertimbangkan. Saya telah melihat banyak ujian negatif melakukan sesuatu seperti: "sediakan hanya apa yang sepatutnya gagal. uji bahawa ia gagal", yang memperkenalkan banyak masalah, terutamanya kerana ia mungkin gagal untuk sebab yang berbeza dan kebanyakan masa ujian ini mengikuti pelaksanaan dalaman kelas dan bukannya keperluannya. Ujian negatif yang betul sebenarnya adalah ujian positif sepenuhnya dengan hanya satu keadaan yang salah. Tidak begitu di sini, untuk kesederhanaan.
Jadi, tanpa berlengah lagi, inilah ujian yang sama, tetapi dengan MockManager:
[TestMethod] public void Calculate_AdditionWorks_MockManager() { // Arrange var mockManager = new CalculatorMockManager() .WithParsedTokens(new List<CalculatorToken> { CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1) }) .WithOperation(OperationType.Add, 1, 1, 2); var service = mockManager.GetService(); // Act service.Calculate(""); //Assert mockManager .VerifyOperationExecute(OperationType.Add, 1, 1, Times.Once); }
Membongkar, tidak ada menyebut tentang cache atau logger, kerana kami tidak memerlukan sebarang persediaan di sana. Semuanya dibungkus dan boleh dibaca. Salin tampal ini dan menukar beberapa parameter atau beberapa baris tidak lagi hodoh. Terdapat tiga kaedah yang dilaksanakan dalam Arrange, satu dalam Act dan satu dalam Assert. Hanya butiran ejekan yang ringkas sahaja yang disarikan: rangka kerja Moq tidak disebutkan di sini. Malah, ujian ini akan kelihatan sama tanpa mengira rangka kerja mengejek yang diputuskan untuk digunakan.
Mari kita lihat kelas MockManager. Sekarang ini akan kelihatan rumit, tetapi ingat bahawa kami hanya menulis ini sekali dan menggunakannya berkali-kali. Keseluruhan kerumitan kelas ada untuk menjadikan ujian unit boleh dibaca oleh manusia, mudah difahami, dikemas kini dan diselenggara.
public class CalculatorMockManager { private readonly Dictionary<OperationType,Mock<IOperation>> operationMocks = new(); public Mock<ITokenParser> TokenParserMock { get; } = new(); public Mock<IMathOperationFactory> MathOperationFactoryMock { get; } = new(); public Mock<ICache> CacheMock { get; } = new(); public Mock<ILogger> LoggerMock { get; } = new(); public CalculatorMockManager WithParsedTokens(List<CalculatorToken> tokens) { TokenParserMock .Setup(m => m.Parse(It.IsAny<string>())) .Returns( new List<CalculatorToken> { CalculatorToken.Addition, CalculatorToken.From(1), CalculatorToken.From(1) } ); return this; } public CalculatorMockManager WithOperation(OperationType operationType, int v1, int v2, int result) { var operationMock = new Mock<IOperation>(); operationMock .Setup(m => m.Execute(v1, v2)) .Returns(result); MathOperationFactoryMock .Setup(m => m.GetOperation(operationType)) .Returns(operationMock.Object); operationMocks[operationType] = operationMock; return this; } public Calculator GetService() { return new Calculator( TokenParserMock.Object, MathOperationFactoryMock.Object, CacheMock.Object, LoggerMock.Object ); } public CalculatorMockManager VerifyOperationExecute(OperationType operationType, int v1, int v2, Func<Times> times) { MathOperationFactoryMock .Verify(m => m.GetOperation(operationType), Times.AtLeastOnce); var operationMock = operationMocks[operationType]; operationMock .Verify(m => m.Execute(v1, v2), times); return this; } }
Semua olok-olok yang diperlukan untuk kelas ujian diisytiharkan sebagai harta awam, membenarkan sebarang penyesuaian ujian unit. Terdapat kaedah GetService, yang akan sentiasa mengembalikan contoh kelas yang diuji, dengan semua kebergantungan diejek sepenuhnya. Kemudian terdapat kaedah With* yang secara atom menyediakan pelbagai senario dan sentiasa mengembalikan pengurus olok-olok, supaya mereka boleh dirantai. Anda juga boleh mempunyai kaedah khusus untuk penegasan, walaupun dalam kebanyakan kes anda akan membandingkan beberapa output dengan nilai yang dijangkakan, jadi ini di sini hanya untuk mengabsahkan kaedah Sahkan rangka kerja Moq.
Corak ini kini menjajarkan penulisan ujian dengan penulisan kod:
Menulis ujian unit sekarang adalah remeh dan konsisten:
Pengabstrakan tidak berhenti pada rangka kerja mengejek. Corak yang sama boleh digunakan dalam setiap bahasa pengaturcaraan! Binaan pengurus olok-olok akan sangat berbeza untuk TypeScript atau JavaScript atau sesuatu yang lain, tetapi ujian unit akan kelihatan dengan cara yang sama.
Semoga ini membantu!
Atas ialah kandungan terperinci MockManager dalam ujian unit - corak pembina yang digunakan untuk olok-olok. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!