Autofixture: Hasilkan objek dengan batasan kompleks

Dibuat pada 26 Agu 2018  ·  8Komentar  ·  Sumber: AutoFixture/AutoFixture

Saya mencoba mencari cara terbaik untuk membuat AutoFixture menghasilkan objek yang tidak memiliki kendala sepele dalam konstruktor. Misalnya, katakanlah saya ingin menggunakan struktur data PrimeNumber yang akan mengambil int dan hanya menerima bilangan prima.

Apa pendekatan terbaik untuk menghasilkan contoh struktur semacam ini di AutoFixture ? Maksud saya, saya jelas akan menulis penyesuaian, tetapi apa yang akan Anda masukkan di sana?

  • Apakah Anda akan menghasilkan int dan loop acak hingga salah satunya adalah bilangan prima (atau tentu saja menjalankan algoritme pembangkit utama)? Itu bisa diterima untuk batasan semacam ini, tetapi jika batasan itu lebih sulit untuk dipatuhi, itu akan cepat menjadi mahal.
  • Apakah Anda akan memberikan daftar terbatas dari beberapa nilai yang dapat diterima?

Selain itu, katakanlah sekarang saya mencoba membuat instance dari sesuatu yang membutuhkan beberapa argumen yang secara teori dapat acak secara individual, tetapi itu akan melakukan beberapa validasi di antaranya (misalnya argA dapat berada dalam kisaran nilai ini hanya jika argB benar, dan argC harus mematuhi aturan validasi yang berbeda tergantung pada nilai argA, atau properti argC.X harus cocok dengan properti argA.X, beberapa hal seperti itu).

Apa yang akan Anda lakukan dalam kasus ini?

  • Satu penyesuaian untuk membuat instance yang valid dari setiap jenis (tanpa mengganggu validasi eksternal apa pun), dan yang lain yang akan mencoba membuat objek kompleks besar, mengulang hingga instance yang valid dibuat?
  • Sekali lagi, berikan daftar nilai terbatas yang dapat diterima, yang dapat menjadi batasan besar dari amplitudo kemungkinan
  • Berikan kustomisasi khusus yang hanya akan membuat instance argumen yang sesuai dengan validasi objek kompleks

Dan akhirnya (saya bisa saja membuat beberapa masalah, tetapi saya merasa semua mata pelajaran itu adalah aspek yang berbeda dari masalah yang sama), harus membuat dan menerapkan penyesuaian semacam ini setiap kali kami menambahkan kelas baru, dan harus mempertahankan penyesuaian itu kapan pun perubahan aturan validasi tampak seperti banyak pekerjaan, apakah Anda menerapkan beberapa teknik untuk mengurangi ini?

Terima kasih banyak, maaf kepanjangan dan semoga postingannya tidak terlalu berantakan.

question

Komentar yang paling membantu

Selamat siang! Akhirnya saya mengalokasikan sedikit jenis untuk menjawab - maaf atas balasan yang sangat terlambat 😊

Pertama-tama perhatikan bahwa inti AutoFixture cukup sederhana dan kami tidak memiliki dukungan bawaan untuk pohon kompleks dengan kendala. Singkatnya, strategi pembuatannya seperti berikut:

  • Cari konstruktor publik atau metode pabrik statis (metode statis mengembalikan turunan dari tipe saat ini).
  • Selesaikan argumen konstruktor dan aktifkan instance.
  • Isi properti dan bidang publik yang dapat ditulis dengan nilai yang dihasilkan.

Dengan pendekatan saat ini, seperti yang Anda lihat sebelumnya, Anda entah bagaimana tidak dapat mengontrol batasan ketergantungan.

Kami memiliki beberapa poin penyesuaian untuk menentukan bagaimana membangun tipe tertentu, tetapi mereka relatif sederhana dan tidak mendukung aturan kompleks tersebut.

Apa pendekatan terbaik untuk menghasilkan contoh struktur semacam ini di AutoFixture ? Maksud saya, saya jelas akan menulis penyesuaian, tetapi apa yang akan Anda masukkan di sana?

  • Apakah Anda akan menghasilkan int dan loop acak hingga salah satunya adalah bilangan prima (atau tentu saja menjalankan algoritme pembangkit utama)? Itu bisa diterima untuk batasan semacam ini, tetapi jika batasan itu lebih sulit untuk dipatuhi, itu akan cepat menjadi mahal.

  • Apakah Anda akan memberikan daftar terbatas dari beberapa nilai yang dapat diterima?

Yah, sayangnya saya tidak melihat peluru perak di sini dan pendekatan tergantung pada situasi. Jika Anda tidak mengandalkan nilai untuk menjadi terlalu acak, atau SUT tunggal hanya menggunakan 1-2 bilangan prima, maka mungkin tidak masalah untuk membuat hardcode bilangan prima dan memilihnya (kami memiliki ElementsBulider<> built-in helper untuk kasus-kasus itu). Di sisi lain, jika Anda memerlukan daftar bilangan prima yang besar dan Anda beroperasi dengan urutan bilangan prima yang panjang, maka mungkin lebih baik untuk membuat kode algoritma untuk menghasilkannya secara dinamis.

Selain itu, katakanlah sekarang saya mencoba membuat instance dari sesuatu yang membutuhkan beberapa argumen yang secara teori dapat acak secara individual, tetapi itu akan melakukan beberapa validasi di antaranya (misalnya argA dapat berada dalam kisaran nilai ini hanya jika argB benar, dan argC harus mematuhi aturan validasi yang berbeda tergantung pada nilai argA, atau properti argC.X harus cocok dengan properti argA.X, beberapa hal seperti itu).

Apa yang akan Anda lakukan dalam kasus ini?

Benar-benar pertanyaan yang bagus dan sayangnya AutoFixture tidak memungkinkan untuk menyelesaikannya dengan cara yang baik di luar kotak. Biasanya saya mencoba mengisolasi penyesuaian untuk setiap jenis, jadi penyesuaian untuk satu jenis mengontrol pembuatan hanya untuk satu jenis. Tetapi dalam kasus saya, tipenya independen dan jelas itu tidak akan berfungsi dengan baik dalam kasus Anda. Juga AutoFixture tidak memberikan konteks di luar kotak, jadi saat Anda menulis penyesuaian untuk jenis tertentu, Anda tidak dapat memahami dengan jelas konteks tempat Anda membuat objek (disebut spesimen secara internal).

Di atas kepala saya, saya akan mengatakan bahwa saya biasanya akan merekomendasikan strategi berikut:

  • Coba buat kustomisasi untuk setiap jenis dengan cara mengontrol pembuatan jenis objek tunggal saja.
  • Jika Anda perlu membuat dependensi dengan batasan tertentu, sebaiknya aktifkan juga dependensi tersebut dalam penyesuaian. Jika ketergantungan Anda bisa berubah, Anda dapat meminta AutoFixture untuk membuat ketergantungan untuk Anda dan kemudian mengonfigurasinya dengan cara yang kompatibel.

Dengan cara ini Anda tidak akan terlalu bertentangan dengan arsitektur internal dan akan jelas cara kerjanya. Tentu saja, berpotensi cara ini sangat bertele-tele.

Jika kasus dengan kendala kompleks tidak begitu umum, kemampuan yang ada mungkin cukup untuk Anda. Tetapi jika model domain Anda benar-benar penuh dengan kasus seperti itu, sejujurnya AutoFixture mungkin bukan alat terbaik untuk Anda. Mungkin, ada alat yang lebih baik di pasar yang memungkinkan untuk memecahkan masalah seperti itu dengan cara yang paling elegan. Tentu saja, perlu disebutkan bahwa AutoFixture sangat fleksibel dan Anda dapat menimpa hampir semuanya, sehingga Anda selalu dapat membuat DSL Anda sendiri di atas inti AutoFixture... Tetapi Anda harus mengevaluasi cara mana yang lebih murah untuk Anda

Yuk minta pendapatnya juga dari

Jika Anda memiliki pertanyaan lebih lanjut - silakan bertanya! Saya akan selalu menyambut untuk menjawabnya.

PS FWIW, saya memutuskan untuk memberi Anda contoh, di mana saya mencoba bermain dengan AutoFixture dan memecahkan masalah serupa (saya mencoba membuatnya tetap sederhana dan mungkin tidak sepenuhnya berfungsi dalam kasus Anda):


Klik untuk melihat kode sumber

```c#
menggunakan Sistem;
menggunakan AutoFixture;
menggunakan AutoFixture.Xunit2;
menggunakan Xunit;

namespace AutoFixturePlayground
{
kelas statis publik Util
{
bool statis publik IsPrime (nomor int)
{
// Disalin dari https://stackoverflow.com/a/15743238/2009373

        if (number <= 1) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        var boundary = (int) Math.Floor(Math.Sqrt(number));

        for (int i = 3; i <= boundary; i += 2)
        {
            if (number % i == 0) return false;
        }

        return true;
    }
}

public class DepA
{
    public int Value { get; set; }
}

public class DepB
{
    public int PrimeNumber { get; }
    public int AnyOtherValue { get; }

    public DepB(int primeNumber, int anyOtherValue)
    {
        if (!Util.IsPrime(primeNumber))
            throw new ArgumentOutOfRangeException(nameof(primeNumber), primeNumber, "Number is not prime.");

        PrimeNumber = primeNumber;
        AnyOtherValue = anyOtherValue;
    }
}

public class DepC
{
    public DepA DepA { get; }
    public DepB DepB { get; }

    public DepC(DepA depA, DepB depB)
    {
        if (depB.PrimeNumber < depA.Value)
            throw new ArgumentException("Second should be larger than first.");

        DepA = depA;
        DepB = depB;
    }

    public int GetPrimeNumber() => DepB.PrimeNumber;
}

public class Issue1067
{
    [Theory, CustomAutoData]
    public void ShouldReturnPrimeNumberFromDepB(DepC sut)
    {
        var result = sut.GetPrimeNumber();

        Assert.Equal(sut.DepB.PrimeNumber, result);
    }
}

public class CustomAutoData : AutoDataAttribute
{
    public CustomAutoData() : base(() =>
    {
        var fixture = new Fixture();

        // Add prime numbers generator, returning numbers from the predefined list
        fixture.Customizations.Add(new ElementsBuilder<PrimeNumber>(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41));

        // Customize DepB to pass prime numbers only to ctor
        fixture.Customize<DepB>(c => c.FromFactory((PrimeNumber pn, int anyNumber) => new DepB(pn, anyNumber)));

        // Customize DepC, so that depA.Value is always less than depB.PrimeNumber
        fixture.Customize<DepC>(c => c.FromFactory((DepA depA, DepB depB, byte diff) =>
        {
            depA.Value = depB.PrimeNumber - diff;
            return new DepC(depA, depB);
        }));

        return fixture;
    })
    {
    }
}

/// <summary>
/// A helper type to represent a prime number, so that you can resolve prime numbers 
/// </summary>
public readonly struct PrimeNumber
{
    public int Value { get; }

    public PrimeNumber(int value)
    {
        Value = value;
    }

    public static implicit operator int(PrimeNumber prime) => prime.Value;
    public static implicit operator PrimeNumber(int value) => new PrimeNumber(value);
}

}
```

Semua 8 komentar

Maaf untuk keheningan radio. Kami masih hidup dan saya akan segera membalas - saya sangat sibuk dengan pekerjaan utama saya akhir-akhir ini. Juga mengerjakan rilis NSubstitute v4, jadi waktu sangat terbatas sumber daya :pensive: Pertanyaannya sulit, jadi ingin memikirkan semua cara yang mungkin sebelum memposting jawabannya.

Terima kasih atas kesabarannya, ikuti terus lagunya :wink:

Hai,
Ada berita tentang itu?
Tidak ada tekanan (saya tahu latihannya , plus itu tidak benar-benar menghalangi, saya hanya benar-benar menyukai beberapa saran yang mendidik), itu hanya untuk mengetahui apakah Anda memiliki visibilitas.
Terima kasih banyak!

Selamat siang! Akhirnya saya mengalokasikan sedikit jenis untuk menjawab - maaf atas balasan yang sangat terlambat 😊

Pertama-tama perhatikan bahwa inti AutoFixture cukup sederhana dan kami tidak memiliki dukungan bawaan untuk pohon kompleks dengan kendala. Singkatnya, strategi pembuatannya seperti berikut:

  • Cari konstruktor publik atau metode pabrik statis (metode statis mengembalikan turunan dari tipe saat ini).
  • Selesaikan argumen konstruktor dan aktifkan instance.
  • Isi properti dan bidang publik yang dapat ditulis dengan nilai yang dihasilkan.

Dengan pendekatan saat ini, seperti yang Anda lihat sebelumnya, Anda entah bagaimana tidak dapat mengontrol batasan ketergantungan.

Kami memiliki beberapa poin penyesuaian untuk menentukan bagaimana membangun tipe tertentu, tetapi mereka relatif sederhana dan tidak mendukung aturan kompleks tersebut.

Apa pendekatan terbaik untuk menghasilkan contoh struktur semacam ini di AutoFixture ? Maksud saya, saya jelas akan menulis penyesuaian, tetapi apa yang akan Anda masukkan di sana?

  • Apakah Anda akan menghasilkan int dan loop acak hingga salah satunya adalah bilangan prima (atau tentu saja menjalankan algoritme pembangkit utama)? Itu bisa diterima untuk batasan semacam ini, tetapi jika batasan itu lebih sulit untuk dipatuhi, itu akan cepat menjadi mahal.

  • Apakah Anda akan memberikan daftar terbatas dari beberapa nilai yang dapat diterima?

Yah, sayangnya saya tidak melihat peluru perak di sini dan pendekatan tergantung pada situasi. Jika Anda tidak mengandalkan nilai untuk menjadi terlalu acak, atau SUT tunggal hanya menggunakan 1-2 bilangan prima, maka mungkin tidak masalah untuk membuat hardcode bilangan prima dan memilihnya (kami memiliki ElementsBulider<> built-in helper untuk kasus-kasus itu). Di sisi lain, jika Anda memerlukan daftar bilangan prima yang besar dan Anda beroperasi dengan urutan bilangan prima yang panjang, maka mungkin lebih baik untuk membuat kode algoritma untuk menghasilkannya secara dinamis.

Selain itu, katakanlah sekarang saya mencoba membuat instance dari sesuatu yang membutuhkan beberapa argumen yang secara teori dapat acak secara individual, tetapi itu akan melakukan beberapa validasi di antaranya (misalnya argA dapat berada dalam kisaran nilai ini hanya jika argB benar, dan argC harus mematuhi aturan validasi yang berbeda tergantung pada nilai argA, atau properti argC.X harus cocok dengan properti argA.X, beberapa hal seperti itu).

Apa yang akan Anda lakukan dalam kasus ini?

Benar-benar pertanyaan yang bagus dan sayangnya AutoFixture tidak memungkinkan untuk menyelesaikannya dengan cara yang baik di luar kotak. Biasanya saya mencoba mengisolasi penyesuaian untuk setiap jenis, jadi penyesuaian untuk satu jenis mengontrol pembuatan hanya untuk satu jenis. Tetapi dalam kasus saya, tipenya independen dan jelas itu tidak akan berfungsi dengan baik dalam kasus Anda. Juga AutoFixture tidak memberikan konteks di luar kotak, jadi saat Anda menulis penyesuaian untuk jenis tertentu, Anda tidak dapat memahami dengan jelas konteks tempat Anda membuat objek (disebut spesimen secara internal).

Di atas kepala saya, saya akan mengatakan bahwa saya biasanya akan merekomendasikan strategi berikut:

  • Coba buat kustomisasi untuk setiap jenis dengan cara mengontrol pembuatan jenis objek tunggal saja.
  • Jika Anda perlu membuat dependensi dengan batasan tertentu, sebaiknya aktifkan juga dependensi tersebut dalam penyesuaian. Jika ketergantungan Anda bisa berubah, Anda dapat meminta AutoFixture untuk membuat ketergantungan untuk Anda dan kemudian mengonfigurasinya dengan cara yang kompatibel.

Dengan cara ini Anda tidak akan terlalu bertentangan dengan arsitektur internal dan akan jelas cara kerjanya. Tentu saja, berpotensi cara ini sangat bertele-tele.

Jika kasus dengan kendala kompleks tidak begitu umum, kemampuan yang ada mungkin cukup untuk Anda. Tetapi jika model domain Anda benar-benar penuh dengan kasus seperti itu, sejujurnya AutoFixture mungkin bukan alat terbaik untuk Anda. Mungkin, ada alat yang lebih baik di pasar yang memungkinkan untuk memecahkan masalah seperti itu dengan cara yang paling elegan. Tentu saja, perlu disebutkan bahwa AutoFixture sangat fleksibel dan Anda dapat menimpa hampir semuanya, sehingga Anda selalu dapat membuat DSL Anda sendiri di atas inti AutoFixture... Tetapi Anda harus mengevaluasi cara mana yang lebih murah untuk Anda

Yuk minta pendapatnya juga dari

Jika Anda memiliki pertanyaan lebih lanjut - silakan bertanya! Saya akan selalu menyambut untuk menjawabnya.

PS FWIW, saya memutuskan untuk memberi Anda contoh, di mana saya mencoba bermain dengan AutoFixture dan memecahkan masalah serupa (saya mencoba membuatnya tetap sederhana dan mungkin tidak sepenuhnya berfungsi dalam kasus Anda):


Klik untuk melihat kode sumber

```c#
menggunakan Sistem;
menggunakan AutoFixture;
menggunakan AutoFixture.Xunit2;
menggunakan Xunit;

namespace AutoFixturePlayground
{
kelas statis publik Util
{
bool statis publik IsPrime (nomor int)
{
// Disalin dari https://stackoverflow.com/a/15743238/2009373

        if (number <= 1) return false;
        if (number == 2) return true;
        if (number % 2 == 0) return false;

        var boundary = (int) Math.Floor(Math.Sqrt(number));

        for (int i = 3; i <= boundary; i += 2)
        {
            if (number % i == 0) return false;
        }

        return true;
    }
}

public class DepA
{
    public int Value { get; set; }
}

public class DepB
{
    public int PrimeNumber { get; }
    public int AnyOtherValue { get; }

    public DepB(int primeNumber, int anyOtherValue)
    {
        if (!Util.IsPrime(primeNumber))
            throw new ArgumentOutOfRangeException(nameof(primeNumber), primeNumber, "Number is not prime.");

        PrimeNumber = primeNumber;
        AnyOtherValue = anyOtherValue;
    }
}

public class DepC
{
    public DepA DepA { get; }
    public DepB DepB { get; }

    public DepC(DepA depA, DepB depB)
    {
        if (depB.PrimeNumber < depA.Value)
            throw new ArgumentException("Second should be larger than first.");

        DepA = depA;
        DepB = depB;
    }

    public int GetPrimeNumber() => DepB.PrimeNumber;
}

public class Issue1067
{
    [Theory, CustomAutoData]
    public void ShouldReturnPrimeNumberFromDepB(DepC sut)
    {
        var result = sut.GetPrimeNumber();

        Assert.Equal(sut.DepB.PrimeNumber, result);
    }
}

public class CustomAutoData : AutoDataAttribute
{
    public CustomAutoData() : base(() =>
    {
        var fixture = new Fixture();

        // Add prime numbers generator, returning numbers from the predefined list
        fixture.Customizations.Add(new ElementsBuilder<PrimeNumber>(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41));

        // Customize DepB to pass prime numbers only to ctor
        fixture.Customize<DepB>(c => c.FromFactory((PrimeNumber pn, int anyNumber) => new DepB(pn, anyNumber)));

        // Customize DepC, so that depA.Value is always less than depB.PrimeNumber
        fixture.Customize<DepC>(c => c.FromFactory((DepA depA, DepB depB, byte diff) =>
        {
            depA.Value = depB.PrimeNumber - diff;
            return new DepC(depA, depB);
        }));

        return fixture;
    })
    {
    }
}

/// <summary>
/// A helper type to represent a prime number, so that you can resolve prime numbers 
/// </summary>
public readonly struct PrimeNumber
{
    public int Value { get; }

    public PrimeNumber(int value)
    {
        Value = value;
    }

    public static implicit operator int(PrimeNumber prime) => prime.Value;
    public static implicit operator PrimeNumber(int value) => new PrimeNumber(value);
}

}
```

Hai @zvirja

Wow, terima kasih untuk jawaban rinci, itu benar-benar menarik. Saya harus melakukan beberapa tes dan memperkirakan apa yang layak dilakukan atau tidak, tetapi secara keseluruhan ini bagus.

Saya tidak berpikir saya memiliki begitu banyak ketergantungan untuk ditangani, jadi pendekatan Anda mungkin merupakan cara yang baik untuk dilakukan. Tentu saja, jika @ploeh kebetulan memiliki sesuatu yang lebih untuk ditambahkan, saya akan merasa terhormat

Terima kasih lagi, terus bekerja dengan baik!

Pengalaman saya dengan AutoFixture dan pengujian berbasis properti adalah bahwa pada dasarnya ada dua cara untuk mengatasi masalah seperti ini:

  • Penyaringan
  • Pembuatan algoritma

(Saat saya menulis, intuisi saya menunjukkan bahwa ini bisa menjadi _catamorphisms_ dan _anamorphisms_, masing-masing, tetapi saya harus memikirkannya lagi, jadi selain ini sebagian besar merupakan catatan untuk diri saya sendiri.)

Jika _most_ nilai yang dihasilkan secara acak akan sesuai dengan batasan apa pun yang harus ditampung seseorang, maka menggunakan generator yang ada, tetapi membuang nilai yang tidak sesuai sesekali, bisa menjadi cara termudah untuk mengatasi masalah tersebut.

Jika, di sisi lain, filter berarti membuang sebagian besar data acak, Anda malah harus membuat algoritme yang, mungkin berdasarkan nilai seed acak, akan menghasilkan nilai yang sesuai dengan batasan yang dimaksud.

Beberapa tahun yang lalu, saya memberikan ceramah yang menunjukkan beberapa contoh sederhana dari kedua pendekatan dalam konteks FsCheck . Presentasi ini sebenarnya merupakan evolusi dari pembicaraan yang mengambil pendekatan yang sama, tetapi hanya dengan AutoFixture saja. Sayangnya, tidak ada rekaman pembicaraan itu.

Seseorang dapat mengatasi persyaratan bilangan prima dengan dua cara.

Pendekatan filter adalah menghasilkan angka yang tidak dibatasi, dan kemudian membuang angka sampai Anda mendapatkan angka yang memang prima.

Pendekatan algoritmik akan menggunakan algoritma seperti saringan utama untuk menghasilkan bilangan prima. Ini tidak acak, jadi orang mungkin ingin mencari cara untuk mengacaknya.

Pertanyaan keseluruhan tentang bagaimana menangani nilai-nilai yang dibatasi dalam AutoFixture muncul segera setelah orang lain mulai melihat perpustakaan, dan saya menulis artikel saat itu yang masih saya rujuk: http://blog.ploeh.dk/2009/ 05/01/Berurusan DenganInput Terkendala

Mengenai pertanyaan tentang beberapa nilai yang berhubungan satu sama lain, saya tidak ingin memberikan panduan umum. Pertanyaan semacam itu sering kali merupakan masalah XY. Dalam banyak kasus, setelah saya memahami secara spesifik, desain alternatif dapat memecahkan masalah tidak hanya dengan AutoFixture, tetapi juga basis kode produksi itu sendiri.

Bahkan di hadapan masalah XY, masih akan ada situasi di mana ini mungkin menjadi masalah yang sah, tetapi saya lebih suka menanganinya berdasarkan kasus per kasus, karena, menurut pengalaman saya, mereka langka.

Jadi jika Anda memiliki contoh spesifik tentang ini, saya mungkin dapat membantu, tetapi saya rasa saya tidak dapat menjawab pertanyaan umum secara bermakna.

@ploeh Terima kasih banyak atas jawaban ini, yang menegaskan pendekatan yang saya pertimbangkan (dan Anda membuat saya penasaran tentang cata- dan anamorphisms ).
Saya sepenuhnya setuju bahwa nilai yang saling bergantung sebagian besar merupakan masalah XY (setidaknya dalam kasus saya), masalahnya adalah ketika mengerjakan kode lama (belum diuji ), berurusan dengan nilai-nilai itu adalah awal yang baik untuk menulis beberapa tes, sampai kita mendapatkan waktu untuk refactor ini dengan benar.

Bagaimanapun, kedua jawaban Anda mengatasi masalah dengan cukup baik, saya kira saya baik untuk pergi dari sana.
Terima kasih!

BTW, saya lupa menyebutkan bahwa saya hanya bermaksud jawaban saya sebagai tambahan untuk @zvirja . Sudah ada jawaban yang bagus di sana 👍

Saya tidak mengambilnya dengan cara lain

Apakah halaman ini membantu?
0 / 5 - 0 peringkat