Bagikan melalui


Praktik terbaik pengujian unit dengan .NET Core dan .NET Standard

Ada banyak manfaat tes unit penulisan; mereka membantu dengan regresi, memberikan dokumentasi, dan memfasilitasi desain yang baik. Namun, tes unit yang sulit dibaca dan rapuh dapat menimbulkan malapetaka pada basis kode Anda. Artikel ini menjelaskan beberapa praktik terbaik mengenai desain pengujian unit untuk proyek .NET Core dan .NET Standard Anda.

Dalam panduan ini, Anda mempelajari beberapa praktik terbaik saat menulis pengujian unit untuk menjaga pengujian Anda tetap tangguh dan mudah dipahami.

Oleh John Reese dengan terima kasih khusus kepada Roy Osherove

Mengapa uji unit?

Ada beberapa alasan untuk menggunakan pengujian unit.

Lebih sedikit waktu melakukan pengujian fungsi

Tes fungsi ini mahal. Mereka biasanya melibatkan pembukaan aplikasi dan melakukan serangkaian langkah yang harus Anda (atau orang lain) ikuti untuk memvalidasi perilaku yang diharapkan. Langkah-langkah ini mungkin tidak selalu diketahui oleh penguji. Mereka harus menjangkau seseorang yang lebih berpengetahuan luas di daerah untuk melakukan tes. Pengujian itu sendiri bisa memakan waktu beberapa detik untuk perubahan sepele, atau menit untuk perubahan yang lebih besar. Terakhir, proses ini harus diulang untuk setiap perubahan yang Anda buat dalam sistem.

Pengujian unit, di sisi lain, mengambil milidetik, dapat dijalankan dengan menekan tombol, dan tidak selalu memerlukan pengetahuan tentang sistem pada umumnya. Apakah tes lolos atau gagal terserah runner pengujian, bukan individu.

Perlindungan terhadap regresi

Cacat regresi adalah cacat yang diperkenalkan ketika perubahan dilakukan pada aplikasi. Penguji tidak hanya menguji fitur baru mereka tetapi juga menguji fitur yang ada sebelumnya untuk memverifikasi bahwa fitur yang diimplementasikan sebelumnya masih berfungsi seperti yang diharapkan.

Dengan pengujian unit, Anda dapat menjalankan ulang seluruh rangkaian pengujian setelah setiap build atau bahkan setelah Anda mengubah baris kode. Memberi Anda keyakinan bahwa kode baru Anda tidak merusak fungsionalitas yang ada.

Dokumentasi yang dapat dieksekusi

Mungkin tidak selalu jelas apa yang dilakukan metode tertentu atau bagaimana perilakunya diberikan input tertentu. Anda mungkin bertanya pada diri sendiri: Bagaimana metode ini berperilaku jika saya meneruskannya string kosong? Null?

Ketika Anda memiliki serangkaian pengujian unit bernama baik, setiap pengujian harus dapat menjelaskan dengan jelas output yang diharapkan untuk input tertentu. Selain itu, harus dapat memverifikasi bahwa itu benar-benar berfungsi.

Kode yang kurang digabungkan

Ketika kode digabungkan dengan erat, mungkin sulit untuk pengujian unit. Tanpa membuat pengujian unit untuk kode yang Anda tulis, kopling mungkin kurang jelas.

Menulis tes untuk kode Anda akan secara alami memisahkan kode Anda, karena akan lebih sulit untuk menguji sebaliknya.

Karakteristik pengujian unit yang baik

  • Cepat: Tidak jarang proyek dewasa memiliki ribuan pengujian unit. Pengujian unit harus membutuhkan sedikit waktu untuk dijalankan. Milidetik.
  • Terisolasi: Pengujian unit bersifat mandiri, dapat dijalankan dalam isolasi, dan tidak memiliki dependensi pada faktor luar seperti sistem file atau database.
  • Dapat diulang: Menjalankan pengujian unit harus konsisten dengan hasilnya, yaitu selalu mengembalikan hasil yang sama jika Anda tidak mengubah apa pun di antara eksekusi.
  • Pemeriksaan Mandiri: Pengujian harus dapat mendeteksi secara otomatis apakah lulus atau gagal tanpa interaksi manusia.
  • Tepat waktu: Pengujian unit tidak boleh memakan waktu yang lama untuk menulis dibandingkan dengan kode yang sedang diuji. Jika Anda menemukan pengujian kode membutuhkan banyak waktu dibandingkan dengan menulis kode, pertimbangkan desain yang lebih dapat diuji.

Cakupan kode

Persentase cakupan kode tinggi sering dikaitkan dengan kualitas kode yang lebih tinggi. Namun, pengukuran itu sendiri tidak dapat menentukan kualitas kode. Menetapkan tujuan persentase cakupan kode yang terlalu ambisius dapat menjadi kontraproduktif. Bayangkan proyek kompleks dengan ribuan cabang bersyarah, dan bayangkan Anda menetapkan tujuan cakupan kode 95%. Saat ini proyek mempertahankan cakupan kode 90%. Jumlah waktu yang diperlukan untuk memperhitungkan semua kasus tepi dalam 5% yang tersisa bisa menjadi usaha besar-besaran, dan proposisi nilai dengan cepat berkurang.

Persentase cakupan kode tinggi bukan indikator keberhasilan, juga tidak menyiratkan kualitas kode tinggi. Ini hanya mewakili jumlah kode yang dicakup oleh pengujian unit. Untuk informasi selengkapnya, lihat cakupan kode pengujian unit.

Mari kita berbicara bahasa yang sama

Istilah tiruan sayangnya sering disalahgunakan ketika berbicara tentang pengujian. Poin-poin berikut menentukan jenis palsu yang paling umum saat menulis pengujian unit:

Palsu - Palsu adalah istilah generik yang dapat digunakan untuk menggambarkan stub atau objek tiruan. Baik itu stub atau tiruan tergantung pada konteks penggunaannya. Jadi dengan kata lain, palsu bisa menjadi stub atau tiruan.

Tiruan - Objek tiruan adalah objek palsu dalam sistem yang memutuskan apakah pengujian unit telah lulus atau gagal atau tidak. Sebuah tiruan dimulai sebagai Palsu sampai dinyatakan melawan.

Stub - Stub adalah pengganti yang dapat dikontrol untuk dependensi yang ada (atau kolaborator) dalam sistem. Dengan menggunakan stub, Anda dapat menguji kode tanpa berurusan dengan dependensi secara langsung. Secara default, stub dimulai sebagai palsu.

Perhatikan cuplikan kode berikut:

var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Contoh sebelumnya adalah stub yang disebut sebagai tiruan. Dalam hal ini, itu adalah sebuah tinta. Anda hanya meneruskan Pesanan sebagai sarana untuk dapat membuat instans Purchase (sistem yang sedang diuji). Namanya MockOrder juga menyesatkan karena sekali lagi, urutannya bukan tiruan.

Pendekatan yang lebih baik adalah:

var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Dengan mengganti nama kelas menjadi FakeOrder, Anda telah membuat kelas jauh lebih umum. Kelas dapat digunakan sebagai tiruan atau stub, mana yang lebih baik untuk kasus pengujian. Dalam contoh sebelumnya, FakeOrder digunakan sebagai stub. Anda tidak menggunakan FakeOrder dalam bentuk atau formulir apa pun selama pernyataan. FakeOrder diteruskan ke Purchase kelas untuk memenuhi persyaratan konstruktor.

Untuk menggunakannya sebagai Mock, Anda dapat melakukan sesuatu seperti kode berikut:

var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

Dalam hal ini, Anda memeriksa properti pada Palsu (menegaskan terhadapnya), jadi dalam cuplikan kode sebelumnya, mockOrder adalah Mock.

Penting

Penting untuk memperbaiki terminologi ini. Jika Anda menyebut "tiruan" stub Anda, pengembang lain akan membuat asumsi palsu tentang niat Anda.

Hal utama yang perlu diingat tentang tiruan versus stub adalah bahwa tiruan sama seperti bongkahan, tetapi Anda menegaskan terhadap objek tiruan, sedangkan Anda tidak menegaskan terhadap bongkahan.

Praktik terbaik

Berikut adalah beberapa praktik terbaik terpenting untuk menulis pengujian unit.

Hindari dependensi infrastruktur

Cobalah untuk tidak memperkenalkan dependensi pada infrastruktur saat menulis pengujian unit. Dependensi membuat pengujian lambat dan rapuh dan harus dicadangkan untuk pengujian integrasi. Anda dapat menghindari dependensi ini dalam aplikasi Anda dengan mengikuti Prinsip Dependensi Eksplisit dan menggunakan Injeksi Dependensi. Anda juga dapat menyimpan pengujian unit dalam proyek terpisah dari pengujian integrasi Anda. Pendekatan ini memastikan proyek pengujian unit Anda tidak memiliki referensi atau dependensi pada paket infrastruktur.

Penamaan pengujian Anda

Nama pengujian Anda harus terdiri dari tiga bagian:

  • Nama metode yang sedang diuji.
  • Skenario di mana ia sedang diuji.
  • Perilaku yang diharapkan ketika skenario dipanggil.

Mengapa?

Standar penamaan penting karena secara eksplisit mengekspresikan niat pengujian. Pengujian lebih dari sekadar memastikan kode Anda berfungsi, mereka juga menyediakan dokumentasi. Hanya dengan melihat rangkaian pengujian unit, Anda harus dapat menyimpulkan perilaku kode Anda tanpa melihat kode itu sendiri. Selain itu, ketika pengujian gagal, Anda dapat melihat dengan tepat skenario mana yang tidak memenuhi harapan Anda.

Buruk:

[Fact]
public void Test_Single()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Lebih:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Mengatur pengujian Anda

Arrange, Act, Assert adalah pola umum saat pengujian unit. Sesuai namanya, ini terdiri dari tiga tindakan utama:

  • Atur objek Anda, buat dan siapkan seperlunya.
  • Bertindak pada objek.
  • Tegaskan bahwa sesuatu seperti yang diharapkan.

Mengapa?

  • Memisahkan dengan jelas apa yang sedang diuji dari langkah-langkah pengaturan dan pernyataan .
  • Lebih sedikit kesempatan untuk intermix pernyataan dengan kode "Act".

Keterbacaan adalah salah satu aspek terpenting saat menulis pengujian. Memisahkan masing-masing tindakan ini dalam pengujian dengan jelas menyoroti dependensi yang diperlukan untuk memanggil kode Anda, bagaimana kode Anda dipanggil, dan apa yang ingin Anda tegaskan. Meskipun mungkin untuk menggabungkan beberapa langkah dan mengurangi ukuran pengujian Anda, tujuan utamanya adalah untuk membuat pengujian sebaca mungkin.

Buruk:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Assert
    Assert.Equal(0, stringCalculator.Add(""));
}

Lebih:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add("");

    // Assert
    Assert.Equal(0, actual);
}

Menulis pengujian yang lolos minimal

Input yang akan digunakan dalam pengujian unit harus menjadi yang paling sederhana untuk memverifikasi perilaku yang saat ini Anda uji.

Mengapa?

  • Pengujian menjadi lebih tahan terhadap perubahan di masa mendatang dalam basis kode.
  • Lebih dekat dengan perilaku pengujian atas implementasi.

Pengujian yang mencakup lebih banyak informasi daripada yang diperlukan untuk lulus pengujian memiliki kemungkinan yang lebih tinggi untuk memperkenalkan kesalahan ke dalam pengujian dan dapat membuat niat pengujian kurang jelas. Saat menulis tes, Anda ingin fokus pada perilaku. Mengatur properti tambahan pada model atau menggunakan nilai non-nol jika tidak diperlukan, hanya mengurangi dari apa yang coba Anda buktikan.

Buruk:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("42");

    Assert.Equal(42, actual);
}

Lebih:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Hindari string ajaib

Penamaan variabel dalam pengujian unit penting, jika tidak lebih penting, daripada penamaan variabel dalam kode produksi. Pengujian unit tidak boleh berisi string ajaib.

Mengapa?

  • Mencegah kebutuhan pembaca pengujian untuk memeriksa kode produksi untuk mencari tahu apa yang membuat nilainya istimewa.
  • Secara eksplisit menunjukkan apa yang Anda coba buktikandaripada mencoba mencapainya.

String ajaib dapat menyebabkan kebingungan pada pembaca pengujian Anda. Jika string terlihat tidak biasa, mereka mungkin bertanya-tanya mengapa nilai tertentu dipilih untuk parameter atau mengembalikan nilai. Jenis nilai string ini dapat menyebabkan mereka melihat lebih dekat detail implementasi, daripada fokus pada pengujian.

Tip

Saat menulis tes, Anda harus bertujuan untuk mengekspresikan niat sebanyak mungkin. Dalam kasus string ajaib, pendekatan yang baik adalah menetapkan nilai-nilai ini ke konstanta.

Buruk:

[Fact]
public void Add_BigNumber_ThrowsException()
{
    var stringCalculator = new StringCalculator();

    Action actual = () => stringCalculator.Add("1001");

    Assert.Throws<OverflowException>(actual);
}

Lebih:

[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
    var stringCalculator = new StringCalculator();
    const string MAXIMUM_RESULT = "1001";

    Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);

    Assert.Throws<OverflowException>(actual);
}

Hindari logika dalam pengujian

Saat menulis pengujian unit Anda, hindari perangkaian string manual, kondisi logis, seperti if, , whilefor, dan switch, dan kondisi lainnya.

Mengapa?

  • Kurangi kesempatan untuk memperkenalkan bug di dalam pengujian Anda.
  • Fokus pada hasil akhir, bukan detail implementasi.

Ketika Anda memperkenalkan logika ke dalam rangkaian pengujian Anda, kemungkinan memperkenalkan bug ke dalamnya meningkat secara dramatis. Tempat terakhir yang ingin Anda temukan bug adalah dalam rangkaian pengujian Anda. Anda harus memiliki tingkat kepercayaan diri yang tinggi bahwa pengujian Anda berfungsi, jika tidak, Anda tidak akan mempercayainya. Pengujian yang tidak Anda percayai, tidak memberikan nilai apa pun. Ketika pengujian gagal, Anda ingin memiliki perasaan bahwa ada sesuatu yang salah dengan kode Anda dan bahwa itu tidak dapat diabaikan.

Tip

Jika logika dalam pengujian Anda tampaknya tidak dapat ditolak, pertimbangkan untuk membagi pengujian menjadi dua atau beberapa pengujian yang berbeda.

Buruk:

[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
    var stringCalculator = new StringCalculator();
    var expected = 0;
    var testCases = new[]
    {
        "0,0,0",
        "0,1,2",
        "1,2,3"
    };

    foreach (var test in testCases)
    {
        Assert.Equal(expected, stringCalculator.Add(test));
        expected += 3;
    }
}

Lebih:

[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add(input);

    Assert.Equal(expected, actual);
}

Lebih suka metode pembantu untuk mengatur dan merobek

Jika Anda memerlukan objek atau status serupa untuk pengujian Anda, lebih suka metode pembantu daripada menggunakan Setup atribut dan Teardown jika ada.

Mengapa?

  • Kurangi kebingungan saat membaca pengujian karena semua kode terlihat dari dalam setiap pengujian.
  • Lebih sedikit kesempatan untuk menyiapkan terlalu banyak atau terlalu sedikit untuk tes yang diberikan.
  • Lebih sedikit kesempatan untuk berbagi status antar pengujian, yang menciptakan dependensi yang tidak diinginkan di antaranya.

Dalam kerangka kerja pengujian unit, Setup dipanggil sebelum setiap pengujian unit dalam rangkaian pengujian Anda. Meskipun beberapa mungkin melihat ini sebagai alat yang berguna, biasanya mengarah ke tes yang kembung dan sulit dibaca. Setiap pengujian umumnya akan memiliki persyaratan yang berbeda untuk memulai dan menjalankan pengujian. Sayangnya, Setup memaksa Anda untuk menggunakan persyaratan yang sama persis untuk setiap pengujian.

Catatan

xUnit telah menghapus SetUp dan TearDown pada versi 2.x

Buruk:

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
// more tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}

Lebih:

[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

    var actual = stringCalculator.Add("0,1");

    Assert.Equal(1, actual);
}
// more tests...
private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

Hindari beberapa tindakan

Saat menulis tes Anda, cobalah untuk hanya menyertakan satu tindakan per pengujian. Pendekatan umum untuk hanya menggunakan satu tindakan meliputi:

  • Buat pengujian terpisah untuk setiap tindakan.
  • Gunakan pengujian berparameter.

Mengapa?

  • Ketika pengujian gagal, jelas tindakan mana yang gagal.
  • Memastikan bahwa pengujian hanya difokuskan pada satu kasus.
  • Memberi Anda seluruh gambar mengapa pengujian Anda gagal.

Beberapa tindakan perlu ditegaskan secara individual dan tidak dijamin bahwa semua Pernyataan akan dijalankan. Di sebagian besar kerangka kerja pengujian unit, setelah Assert gagal dalam pengujian unit, pengujian lanjutan secara otomatis dianggap gagal. Proses semacam ini dapat membingungkan karena fungsionalitas yang benar-benar berfungsi, akan ditampilkan sebagai gagal.

Buruk:

[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
    // Act
    var actual1 = stringCalculator.Add("");
    var actual2 = stringCalculator.Add(",");

    // Assert
    Assert.Equal(0, actual1);
    Assert.Equal(0, actual2);
}

Lebih:

[Theory]
[InlineData("", 0)]
[InlineData(",", 0)]
public void Add_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add(input);

    // Assert
    Assert.Equal(expected, actual);
}

Memvalidasi metode privat dengan menguji metode publik unit

Dalam kebanyakan kasus, seharusnya tidak ada kebutuhan untuk menguji metode privat. Metode privat adalah detail implementasi dan tidak pernah ada dalam isolasi. Pada titik tertentu, akan ada metode yang menghadap publik yang memanggil metode privat sebagai bagian dari implementasinya. Apa yang harus Anda pedulikan adalah hasil akhir dari metode publik yang memanggil ke dalam metode privat.

Pertimbangkan kasus berikut:

public string ParseLogLine(string input)
{
    var sanitizedInput = TrimInput(input);
    return sanitizedInput;
}

private string TrimInput(string input)
{
    return input.Trim();
}

Reaksi pertama Anda mungkin mulai menulis tes TrimInput karena Anda ingin memastikan bahwa metode berfungsi seperti yang diharapkan. Namun, sangat mungkin bahwa ParseLogLine memanipulasi sanitizedInput sededaya yang tidak Anda harapkan, merender pengujian terhadap TrimInput tidak berguna.

Tes nyata harus dilakukan terhadap metode ParseLogLine yang dihadapi publik karena itulah yang pada akhirnya harus Anda pedulikan.

public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
    var parser = new Parser();

    var result = parser.ParseLogLine(" a ");

    Assert.Equals("a", result);
}

Dengan sudut pandang ini, jika Anda melihat metode privat, temukan metode publik dan tulis pengujian Anda terhadap metode tersebut. Hanya karena metode privat mengembalikan hasil yang diharapkan, bukan berarti sistem yang akhirnya memanggil metode privat menggunakan hasilnya dengan benar.

Referensi statis stub

Salah satu prinsip pengujian unit adalah bahwa ia harus memiliki kontrol penuh terhadap sistem yang sedang diuji. Prinsip ini dapat bermasalah ketika kode produksi mencakup panggilan ke referensi statis (misalnya, DateTime.Now). Pertimbangkan gambar berikut:

public int GetDiscountedPrice(int price)
{
    if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

Bagaimana kode ini mungkin dapat diuji unit? Anda mungkin mencoba pendekatan seperti:

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(2, actual)
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(1, actual);
}

Sayangnya, Anda akan dengan cepat menyadari bahwa ada beberapa masalah dengan tes Anda.

  • Jika rangkaian pengujian dijalankan pada hari Selasa, pengujian kedua akan lulus, tetapi pengujian pertama akan gagal.
  • Jika rangkaian pengujian dijalankan pada hari lain, pengujian pertama akan lulus, tetapi pengujian kedua akan gagal.

Untuk mengatasi masalah ini, Anda harus memperkenalkan jahitan ke dalam kode produksi Anda. Salah satu pendekatannya adalah membungkus kode yang perlu Anda kontrol dalam antarmuka dan memiliki kode produksi tergantung pada antarmuka tersebut.

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}

public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
    if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

Rangkaian pengujian Anda sekarang menjadi sebagai berikut:

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(2, actual);
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(1, actual);
}

Sekarang rangkaian pengujian memiliki kontrol penuh atas DateTime.Now dan dapat membagi nilai apa pun saat memanggil ke metode .