Bagikan melalui


Gunakan shim untuk mengisolasi aplikasi Anda untuk pengujian unit

Jenis shim, salah satu dari dua teknologi utama yang digunakan oleh Microsoft Fakes Framework, berperan penting dalam mengisolasi komponen aplikasi Anda selama pengujian. Mereka bekerja dengan mencegat dan mengalihkan panggilan ke metode tertentu, yang kemudian dapat Anda arahkan ke kode kustom dalam pengujian Anda. Fitur ini memungkinkan Anda mengelola hasil metode ini, memastikan hasilnya konsisten dan dapat diprediksi selama setiap panggilan, terlepas dari kondisi eksternal. Tingkat kontrol ini menyederhanakan proses pengujian dan membantu dalam mencapai hasil yang lebih andal dan akurat.

Gunakan shim ketika Anda perlu membuat batas antara kode dan rakitan Anda yang tidak menjadi bagian dari solusi Anda. Ketika tujuannya adalah untuk mengisolasi komponen solusi Anda satu sama lain, penggunaan stub disarankan .

(Untuk deskripsi yang lebih rinci untuk rinci, lihat Gunakan stub untuk mengisolasi bagian aplikasi Anda satu sama lain untuk pengujian unit.)

Batasan Shims

Penting untuk dicatat bahwa shim memang memiliki batasannya.

Shim tidak dapat digunakan pada semua jenis dari pustaka tertentu di kelas dasar .NET, khususnya mscorlib dan System dalam .NET Framework, dan di System.Runtime di .NET Core atau .NET 5+. Kendala ini harus diperhitungkan selama tahap perencanaan dan desain pengujian untuk memastikan strategi pengujian yang sukses dan efektif.

Membuat Shim: Panduan Langkah demi Langkah

Misalkan komponen Anda berisi panggilan ke System.IO.File.ReadAllLines:

// Code under test:
this.Records = System.IO.File.ReadAllLines(path);

Membuat Pustaka Kelas

  1. Buka Visual Studio dan buat Class Library proyek

    Cuplikan layar proyek Pustaka Kelas NetFramework di Visual Studio.

  2. Atur nama proyek HexFileReader

  3. Atur nama ShimsTutorialsolusi .

  4. Atur kerangka kerja target proyek ke .NET Framework 4.8

  5. Menghapus file default Class1.cs

  6. Tambahkan file HexFile.cs baru dan tambahkan definisi kelas berikut:

    // HexFile.cs
    public class HexFile
    {
        public string[] Records { get; private set; }
    
        public HexFile(string path)
        {
            this.Records = System.IO.File.ReadAllLines(path);
        }
    }
    

Membuat Proyek Pengujian

  1. Klik kanan pada solusi dan tambahkan proyek baru MSTest Test Project

  2. Atur nama proyek TestProject

  3. Atur kerangka kerja target proyek ke .NET Framework 4.8

    Cuplikan layar proyek NetFramework Test di Visual Studio.

Tambahkan Rakitan Fake

  1. Menambahkan referensi proyek ke HexFileReader

    Cuplikan layar perintah Tambahkan Referensi Proyek.

  2. Tambahkan Rakitan Fake

    • Dalam Penjelajah Solusi,

      • Untuk Proyek .NET Framework yang lebih lama (gaya non-SDK), perluas node Referensi proyek pengujian unit Anda.

      • Untuk proyek gaya SDK yang menargetkan .NET Framework, .NET Core, atau .NET 5+, perluas simpul Dependensi untuk menemukan rakitan yang ingin Anda palsukan di bawah Rakitan, Proyek, atau Paket.

      • Jika Anda bekerja di Visual Basic, pilih Perlihatkan Semua File di toolbar Penjelajah Solusi untuk melihat node Referensi.

    • Pilih rakitan System yang berisi definisi System.IO.File.ReadAllLines.

    • Pada menu pintasan, pilih Tambahkan Rakitan Fake.

    Cuplikan layar perintah Tambahkan Rakitan Palsu.

Karena membangun menghasilkan beberapa peringatan dan kesalahan karena tidak semua jenis dapat digunakan dengan shim, Anda harus memodifikasi konten Fakes\mscorlib.fakes untuk mengecualikannya.

<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true">
  <Assembly Name="mscorlib" Version="4.0.0.0"/>
  <StubGeneration>
    <Clear/>
  </StubGeneration>
  <ShimGeneration>
    <Clear/>
    <Add FullName="System.IO.File"/>
    <Remove FullName="System.IO.FileStreamAsyncResult"/>
    <Remove FullName="System.IO.FileSystemEnumerableFactory"/>
    <Remove FullName="System.IO.FileInfoResultHandler"/>
    <Remove FullName="System.IO.FileSystemInfoResultHandler"/>
    <Remove FullName="System.IO.FileStream+FileStreamReadWriteTask"/>
    <Remove FullName="System.IO.FileSystemEnumerableIterator"/>
  </ShimGeneration>
</Fakes>

Membuat proyek pengujian unit

  1. Ubah file UnitTest1.cs default untuk menambahkan yang berikut ini TestMethod

    [TestMethod]
    public void TestFileReadAllLine()
    {
        using (ShimsContext.Create())
        {
            // Arrange
            System.IO.Fakes.ShimFile.ReadAllLinesString = (s) => new string[] { "Hello", "World", "Shims" };
    
            // Act
            var target = new HexFile("this_file_doesnt_exist.txt");
    
            Assert.AreEqual(3, target.Records.Length);
        }
    }
    

    Berikut adalah Penjelajah Solusi memperlihatkan semua file

    Cuplikan layar Penjelajah Solusi memperlihatkan semua file.

  2. Buka Test Explorer dan jalankan pengujian.

Sangat penting untuk membuang setiap konteks shim dengan benar. Sebagai aturan praktis, panggil bagian ShimsContext.Create dalam pernyataan using untuk memastikan pembersihan yang tepat dari shim terdaftar. Misalnya, Anda dapat mendaftarkan shim untuk metode pengujian yang menggantikan metode DateTime.Now dengan delegasi yang selalu mengembalikan yang pertama Januari 2000. Jika Anda lupa menghapus shim terdaftar dalam metode pengujian, sisa uji coba akan selalu mengembalikan yang pertama Januari 2000 sebagai nilai DateTime.Now. Ini mungkin mengejutkan dan membingungkan.


Konvensi Penamaan untuk Kelas Shim

Nama kelas Shim dibuat dengan mengimbuhi prefiks Fakes.Shim ke nama tipe aslinya. Nama parameter ditambahkan ke nama metode. (Anda tidak perlu menambahkan referensi perakitan apa pun ke System.Fakes.)

    System.IO.File.ReadAllLines(path);
    System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };

Memahami Cara Kerja Shims

Shims beroperasi dengan memperkenalkan memutar ke basis kode aplikasi yang sedang diuji. Setiap kali ada panggilan ke metode asli, sistem Fakes turun tangan untuk mengalihkan panggilan tersebut, menyebabkan kode shim kustom Anda dijalankan alih-alih metode asli.

Penting untuk dicatat bahwa memutar ini dibuat dan dihapus secara dinamis saat runtime. Memutar harus selalu dibuat dalam masa ShimsContextpakai . Ketika ShimsContext dibuang, shim aktif apa pun yang dibuat di dalamnya juga dihapus. Untuk mengelola ini secara efisien, disarankan untuk merangkum pembuatan memutar dalam pernyataan using .


Shim untuk berbagai jenis metode

Shim mendukung berbagai jenis metode.

Metode statis

Saat menggigil metode statis, properti yang menyimpan shim ditempatkan dalam jenis shim. Properti ini hanya memiliki setter, yang digunakan untuk melampirkan delegasi ke metode yang ditargetkan. Misalnya, jika kita memiliki kelas yang disebut MyClass dengan metode MyMethodstatis :

//code under test
public static class MyClass {
    public static int MyMethod() {
        ...
    }
}

Kita dapat melampirkan shim semikian MyMethod rupa sehingga terus-menerus mengembalikan 5:

// unit test code
ShimMyClass.MyMethod = () => 5;

Metode instans (untuk semua instans)

Sama seperti metode statis, metode instans juga dapat dikikis untuk semua instans. Properti yang menyimpan shim ini ditempatkan dalam jenis berlapis bernama AllInstances untuk mencegah kebingungan. Jika kita memiliki kelas MyClass dengan metode MyMethodinstans :

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

Kita dapat melampirkan shim agar MyMethod secara konsisten mengembalikan 5, terlepas dari instansnya:

// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;

Struktur ShimMyClass jenis yang dihasilkan akan muncul sebagai berikut:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public static class AllInstances {
        public static Func<MyClass, int>MyMethod {
            set {
                ...
            }
        }
    }
}

Dalam skenario ini, Fakes meneruskan instans runtime sebagai argumen pertama delegasi.

Metode instans (Instans Runtime Tunggal)

Metode instans juga dapat dikilas menggunakan delegasi yang berbeda, tergantung pada penerima panggilan. Ini memungkinkan metode instans yang sama untuk menunjukkan perilaku yang berbeda per instans jenis. Properti yang menyimpan shim ini adalah metode instans dari jenis shim itu sendiri. Setiap jenis shim yang diinisiasi ditautkan ke instans mentah dari jenis berkilauan.

Misalnya, kelas MyClass dengan metode instans MyMethod:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

Kita dapat membuat dua jenis shim sedemian MyMethod rupa sehingga yang pertama secara konsisten mengembalikan 5 dan yang kedua secara konsisten mengembalikan 10:

// unit test code
var myClass1 = new ShimMyClass()
{
    MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };

Struktur ShimMyClass jenis yang dihasilkan akan muncul sebagai berikut:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public Func<int> MyMethod {
        set {
            ...
        }
    }
    public MyClass Instance {
        get {
            ...
        }
    }
}

Instans jenis yang di-shim aktual dapat diakses melalui properti Instans:

// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;

Jenis shim juga mencakup konversi implisit ke jenis berkilauan, memungkinkan Anda untuk menggunakan jenis shim secara langsung:

// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime instance

Konstruktor

Konstruktor tidak terkecuali berkilauan; mereka juga dapat dikilas untuk melampirkan jenis shim ke objek yang akan dibuat di masa depan. Misalnya, setiap konstruktor diwakili sebagai metode statis, bernama Constructor, dalam jenis shim. Mari kita pertimbangkan kelas MyClass dengan konstruktor yang menerima bilangan bulat:

public class MyClass {
    public MyClass(int value) {
        this.Value = value;
    }
    ...
}

Jenis shim untuk konstruktor dapat disiapkan sedih sehingga, terlepas dari nilai yang diteruskan ke konstruktor, setiap instans di masa mendatang akan mengembalikan -5 ketika value getter dipanggil:

// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
    var shim = new ShimMyClass(@this) {
        ValueGet = () => -5
    };
};

Setiap jenis shim mengekspos dua jenis konstruktor. Konstruktor default harus digunakan ketika instans baru diperlukan, sedangkan konstruktor yang mengambil instans berkilauan sebagai argumen hanya boleh digunakan dalam shim konstruktor:

// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

Struktur jenis yang dihasilkan untuk ShimMyClass dapat diilustrasikan sebagai berikut:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
    public static Action<MyClass, int> ConstructorInt32 {
        set {
            ...
        }
    }

    public ShimMyClass() { }
    public ShimMyClass(MyClass instance) : base(instance) { }
    ...
}

Mengakses anggota Dasar

Sifat shim anggota dasar dapat dicapai dengan membuat shim untuk jenis dasar dan memasukkan instans anak ke dalam konstruktor kelas shim dasar.

Misalnya, pertimbangkan kelas MyBase dengan metode MyMethod instans dan subjenis MyChild:

public abstract class MyBase {
    public int MyMethod() {
        ...
    }
}

public class MyChild : MyBase {
}

Shim MyBase dapat disiapkan dengan memulai shim baru ShimMyBase :

// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };

Penting untuk dicatat bahwa ketika diteruskan sebagai parameter ke konstruktor shim dasar, jenis shim anak secara implisit dikonversi ke instans anak.

Struktur jenis yang dihasilkan untuk ShimMyChild dan ShimMyBase dapat diibaratkan dengan kode berikut:

// Fakes generated code
public class ShimMyChild : ShimBase<MyChild> {
    public ShimMyChild() { }
    public ShimMyChild(Child child)
        : base(child) { }
}
public class ShimMyBase : ShimBase<MyBase> {
    public ShimMyBase(Base target) { }
    public Func<int> MyMethod
    { set { ... } }
}

Konstruktor statik

Jenis Shim mengekspos metode StaticConstructor statis untuk membuat shim konstruktor statis dari suatu jenis. Karena konstruktor statis dijalankan hanya satu kali, Anda perlu memastikan bahwa shim dikonfigurasi sebelum anggota jenisnya diakses.

Finalizer

Finalizer tidak didukung dalam Fakes.

Metode privat

Generator kode Fakes membuat properti shim untuk metode privat yang hanya memiliki jenis yang terlihat dalam tanda tangan, yaitu jenis parameter dan jenis pengembalian terlihat.

Antarmuka pengikatan

Ketika jenis yang di-shim menerapkan antarmuka, generator kode memancarkan metode yang memungkinkannya untuk mengikat semua anggota dari antarmuka tersebut sekaligus.

Misalnya, diberikan kelas MyClass yang mengimplementasikan IEnumerable<int>:

public class MyClass : IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() {
        ...
    }
    ...
}

Anda dapat mengikat implementasi IEnumerable<int> di MyClass dengan memanggil metode Bind:

// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });

Struktur ShimMyClass jenis yang dihasilkan menyerupan kode berikut:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public ShimMyClass Bind(IEnumerable<int> target) {
        ...
    }
}

Mengubah Perilaku Default

Setiap jenis shim yang dihasilkan mencakup instans IShimBehavior antarmuka, yang dapat diakses melalui ShimBase<T>.InstanceBehavior properti . Perilaku ini dipanggil setiap kali klien memanggil anggota instans yang belum berkilauan secara eksplisit.

Secara default, jika tidak ada perilaku tertentu yang telah ditetapkan, ia menggunakan instans yang dikembalikan oleh properti statis ShimBehaviors.Current , yang biasanya melempar NotImplementedException pengecualian.

Anda dapat memodifikasi perilaku ini kapan saja dengan menyesuaikan InstanceBehavior properti untuk instans shim apa pun. Misalnya, cuplikan kode berikut mengubah perilaku untuk tidak melakukan apa-apa atau mengembalikan nilai default dari jenis pengembalian—yaitu: default(T)

// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;

Anda juga dapat mengubah perilaku secara global untuk semua instans yang dikilauan—di mana InstanceBehavior properti belum ditentukan secara eksplisit—dengan mengatur properti statis ShimBehaviors.Current :

// unit test code
// change default shim for all shim instances where the behavior has not been set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;

Mengidentifikasi Interaksi dengan Dependensi Eksternal

Untuk membantu mengidentifikasi kapan kode Anda berinteraksi dengan sistem eksternal atau dependensi (disebut sebagai environment), Anda dapat menggunakan shim untuk menetapkan perilaku tertentu kepada semua anggota jenis. Ini termasuk metode statis. Dengan mengatur ShimBehaviors.NotImplemented perilaku pada properti statis Behavior jenis shim, akses apa pun ke anggota jenis tersebut yang belum dikilap secara eksplisit akan melempar NotImplementedException. Ini dapat berfungsi sebagai sinyal yang berguna selama pengujian, menunjukkan bahwa kode Anda mencoba mengakses sistem eksternal atau dependensi.

Berikut adalah contoh cara menyiapkan ini di kode pengujian unit Anda:

// unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;

Untuk kenyamanan, metode singkat juga disediakan untuk mencapai efek yang sama:

// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();

Memanggil Metode Asli dari Dalam Metode Shim

Mungkin ada skenario di mana Anda mungkin perlu menjalankan metode asli selama eksekusi metode shim. Misalnya, Anda mungkin ingin menulis teks ke sistem file setelah memvalidasi nama file yang diteruskan ke metode .

Salah satu pendekatan untuk menangani situasi ini adalah merangkum panggilan ke metode asli menggunakan delegasi dan ShimsContext.ExecuteWithoutShims(), seperti yang ditunjukkan dalam kode berikut:

// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
  ShimsContext.ExecuteWithoutShims(() => {

      Console.WriteLine("enter");
      File.WriteAllText(fileName, content);
      Console.WriteLine("leave");
  });
};

Atau, Anda dapat membatalkan shim, memanggil metode asli, lalu memulihkan shim.

// unit test code
ShimsDelegates.Action<string, string> shim = null;
shim = (fileName, content) => {
  try {
    Console.WriteLine("enter");
    // remove shim in order to call original method
    ShimFile.WriteAllTextStringString = null;
    File.WriteAllText(fileName, content);
  }
  finally
  {
    // restore shim
    ShimFile.WriteAllTextStringString = shim;
    Console.WriteLine("leave");
  }
};
// initialize the shim
ShimFile.WriteAllTextStringString = shim;

Menangani Konkurensi dengan Jenis Shim

Jenis shim beroperasi di semua utas dalam AppDomain dan tidak memiliki afinitas utas. Properti ini sangat penting untuk diingat jika Anda berencana untuk menggunakan runner uji yang mendukung konkurensi. Perlu dicatat bahwa pengujian yang melibatkan jenis shim tidak dapat berjalan bersamaan, meskipun pembatasan ini tidak diberlakukan oleh runtime Fakes.

Sistem Shimming.Environment

Jika Anda ingin menggigil System.Environment kelas, Anda harus membuat beberapa modifikasi pada mscorlib.fakes file. Mengikuti elemen Assembly, tambahkan konten berikut:

<ShimGeneration>
    <Add FullName="System.Environment"/>
</ShimGeneration>

Setelah Anda membuat perubahan ini dan membangun kembali solusi, metode dan properti di System.Environment kelas sekarang tersedia untuk dikilau. Berikut adalah contoh bagaimana Anda dapat menetapkan perilaku ke GetCommandLineArgsGet metode :

System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...

Dengan membuat modifikasi ini, Anda telah membuka kemungkinan untuk mengontrol dan menguji bagaimana kode Anda berinteraksi dengan variabel lingkungan sistem, alat penting untuk pengujian unit yang komprehensif.