Back to blog
Apr 24, 2023
5 min read

Mocking Django Model and Django FileSystemStorage

How to mock Django model and FileSystemStorage.

Sebagai software developer, tentunya kita sudah tak asing lagi dengan proses testing pada aplikasi. Tapi apakah test yang kita buat telah memenuhi best practice? Pada artikel ini, akan dibahas penerapan mocking untuk mendukung proses testing.

Mocking Django Model

a clean unit test should have five properties: Fast, Independent, Repeatable, Self-Validating, and, Timely.

Clean Code, A Handbook of Agile Software Craftsmanship by Robert C. Martin (Uncle Bob)

Ketika kita membuat unit test, tidak jarang kita melakukan suatu hal yang bersifat kontra-produktif terhadap properti-properti suatu clean unit test. Sebagai contoh, berikut adalah potongan kode dari proyek PPL yang sedang saya kerjakan.

class Applicant(models.Model):
    fullname = models.CharField(max_length=100)
    application_status = models.CharField(max_length=100)
    birth_place = models.CharField(max_length=100)

class ApplicantFile(models.Model):
    applicant = models.ForeignKey(Applicant, on_delete=models.CASCADE)
    file = models.FileField(upload_to=generate_path)

    def __str__(self):
    return f"{self.applicant.fullname} - {self.file.name}"

Jika kita ingin melakukan test terhadap str method pada model ApplicantFile, maka kita dapat melakukan testing seperti berikut

def test_applicantfile_str_method(self):
    applicant = Applicant.objects.create(
        fullname = "John Doe",
        application_status = "Pending",
        birth_place = "Jakarta"
    )
    file = SimpleUploadedFile("file.txt", b"file_content")
    applicantfile = ApplicantFile.objects.create(
        applicant = applicant,
        file = file
    )
    self.assertEqual(str(applicantfile), f"{applicant.fullname} - {file.name}")

Dapatkah anda menunjukkan kekurangan dari unit test diatas? Unit test untuk model ApplicantFile menjadi dependent terhadap model Applicant. Sehingga, jika terdapat kegagalan pada model Applicant, maka unit test untuk model ApplicantFile juga akan ikut gagal.

Selain itu, untuk menjalankan unit test diatas, kita harus membuat 2 object dan memasukkannya ke database. Hal tersebut dapat memperlambat proses testing, terutama jika object yang dibuat bersifat kompleks.

Kita dapat mengatasi masalah tersebut dengan menerapkan mocking. Dengan mocking, kita dapat mensimulasikan object lain tanpa harus membuat object tersebut. Berikut adalah penerapan mocking pada kode unit test sebelumnnya

def test_applicantfile_str_method(self):
    applicant = mock.Mock(spec=Applicant)
    applicant._state = mock.Mock()
    applicant.name = "John Doe"
    file = SimpleUploadedFile("file.txt", b"file_content")
    applicantfile = ApplicantFile.objects.create(
        applicant = applicant,
        file = file
    )
    self.assertEqual(str(applicantfile), f"{applicant.fullname} - {file.name}")

Seperti yang terlihat pada kode diatas, kita melakukan mocking terhadap object Applicant, memberikan object mock tersebut properti yang dibutuhkan, dan menggunakan object mock tersebut dalam pembuatan object ApplicantFile. Dengan mocking, kita telah menyelesaikan masalah pada unit test kita sebelumnnya dengan cara:

  • Melakukan test menggunakan object mock dari model Applicant (tanpa membuat _object A_pplicant model Django) sehingga proses testing tidak perlu untuk menambahkan _object A_pplicant ke database
  • Unit test menjadi independen terhadap implementasi dari model Applicant, sehingga unit test kita akan selalu berjalan tanpa tergantung terhadap model Applicant

https://dareenzo.github.io/blog/2018/10/24/test-doubles-a-primer/

Mocking Django FileSystemStorage

Suatu unit test hendaknya dapat berjalan dengan baik tanpa dipengaruhi hal-hal eksternal seperti API service call atau environment aplikasi. Namun, bagaimana jika kita ingin melakukan testing terhadap behavior aplikasi yang berkaitan dengan hal-hal tersebut? Sebagai contoh, berikut adalah potongan kode dari proyek PPL yang sedang saya kerjakan

@route.post('{applicant_id}/file', response={200: Message})
def upload_applicant_file(self, request, applicant_id: int, files: List[UploadedFile] = File(...)):
    applicant = get_object_or_404(Applicant, id=applicant_id)
    for file in files:
        ApplicantFile.objects.create(applicant=applicant, file=file)
    applicant.update_date = timezone.now()
    applicant.update_by = request.user
    applicant.save()
    return {"message": "Files successfully uploaded"}

Kode diatas merupakan implementasi dari salah satu API endpoint pada proyek saya yang berfungsi meng-handle upload file suatu Applicant dan menyimpanya ke storage/database. Jika kita ingin melakukan testing terhadap function tersebut, kita dapat mengimplementasikannya sebagai berikut

def test_upload_applicant_file(self):
    applicant = Applicant.objects.create(
        fullname = "John Doe",
        application_status = "Pending",
        birth_place = "Jakarta"
    )
    file = SimpleUploadedFile("file.txt", b"file_content")
    response = self.client.post(f"/api/applicant/{applicant.id}/file", data={'files': [file]})
    self.assertEqual(response.status_code, 200)
    self.assertEqual(response.json(), {"message": "Files successfully uploaded"})
    self.assertTrue(ApplicantFile.objects.filter(applicant=self.applicant).exists())

Ketika test tersebut dijalankan, Django akan membuat file baru pada direktori yang telah ditentukan (pada kasus ini media/applicant_files_1/file.txt). File tersebut akan tetap ada walaupun setelah testing berakhir kecuali dilakukan penghapusan file secara khusus setiap testing diselesaikan. Hal ini berpotensi menyebabkan masalah. Dengan menggunakan mocking, kita dapat mengatasi hal tersebut

@patch.object(default_storage, 'save', MagicMock(return_value='applicant_files/1/file.txt'))
def test_upload_applicant_file(self):
    applicant = Applicant.objects.create(
        fullname = "John Doe",
        application_status = "Pending",
        birth_place = "Jakarta"
    )
    file = SimpleUploadedFile("file.txt", b"file_content")
    response = self.client.post(f"/api/applicant/{applicant.id}/file", data={'files': [file]})
    self.assertEqual(response.status_code, 200)
    self.assertEqual(response.json(), {"message": "Files successfully uploaded"})
    self.assertTrue(ApplicantFile.objects.filter(applicant=applicant).exists())

Pada kode diatas, kita melakukan mocking terhadap class default storage Django yaitu FileSystemStorage tepatnya pada save method dari class tersebut. Mocking dilakukan pada save method sehingga ketika method tersebut dipanggil (pada pembuatan object), file tidak akan tersimpan ke dalam file system. Hal tersebut bertujuan untuk membuat file system tetap bersih setelah dilakukan testing dan mempercepat testing.

Kesimpulan

Mocking dapat membantu kita dalam membuat unit test yang sesuai dengan kaidah clean unit test. Selain itu, mocking juga dapat dilakukan terhadap hal-hal eksternal seperti pemanggilan API service dan file system.