supabase-community / supabase-kt

A Kotlin Multiplatform Client for Supabase.
https://supabase.com/docs/reference/kotlin/introduction
MIT License
406 stars 37 forks source link

[Question]: Most functions from PostgreSQLBuilder are inline, making the class very hard to unit test. Do you have recommendations? #298

Closed jlengrand closed 1 year ago

jlengrand commented 1 year ago

General info

What is your question?

Hey there!

See this question for context.

I'm using the library and I want to unit test my code, reason for which I am trying to mock the SupabaseClient.

Thing is, because functions in PostgrestBuilder.kt are inlined they cannot be mocked by definitions, which makes it hard to unit test.

On top of this, because Supabase does not seem to offer test containers, I cannot turn myself to integration tests either.

Do you have any recommendations as to what to do?

Thanks!

jlengrand commented 1 year ago

BTW, I made a minimally reproduceable example over here : https://github.com/jlengrand/supabase-mock-demo-kotlin.

No need for any db or db credentials to run it

jlengrand commented 1 year ago

My current method is to wrap your library in a class and mock that class instead, but it looks more like a workaround than a solution

https://stackoverflow.com/a/77188050/282677

jan-tennert commented 1 year ago

Hey! Sadly, I currently don't have a good solution for that problem. The only two solutions I can think of is your current approach and using a mocked http engine for ktor, which you can provide using

val client = createSupabaseClient(
    supabaseUrl,
    supabaseKey,
) {
    httpEngine = MockEngine { request -> //this will get executed everytime a ktor gets used by any function
        respond(Json.encodeToString(listOf<ResultPerson>()))
    }
}

But both obviously aren't ideal, and I'm not sure what I could do to make this easier.

hieuwu commented 1 year ago

My current method is to wrap your library in a class and mock that class instead, but it looks more like a workaround than a solution

https://stackoverflow.com/a/77188050/282677

Based on the question. Could you please try providing the Posgrest instance instead of the client to where you want to call the API? It would be easier to mock the dependency when write test

jlengrand commented 1 year ago

Coming back here just to mentioned that I decided to go another direction, which I'm pretty satisfied with actually. I created a simple Docker Compose setup that I run with test containers and which simulate a simple Supabase instance. No need for mocking any more. It's more "integration tests" than "unit tests" at that point though.

The full blog about it, but in short :

version: '3'

# Thanks https://github.com/mattddowney/compose-postgrest/blob/master/README.md
services:

  ################
  # postgrest-db #
  ################
  postgrest-db:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
      - DB_SCHEMA=${DB_SCHEMA}
    volumes:
      - "./initdb:/docker-entrypoint-initdb.d"
    networks:
      - postgrest-backend
    restart: always

  #############
  # postgrest #
  #############
  postgrest:
    image: postgrest/postgrest:latest
    ports:
      - "3000:3000"
    environment:
      - PGRST_DB_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgrest-db:5432/${POSTGRES_DB}
      - PGRST_DB_SCHEMA=${DB_SCHEMA}
      - PGRST_DB_ANON_ROLE=${DB_ANON_ROLE}
      - PGRST_JWT_SECRET=${PGRST_JWT_SECRET}
    networks:
      - postgrest-backend
    restart: always

  #############
  # Nginx     #
  #############
  nginx:
    image: nginx:alpine
    restart: always
    tty: true
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "80:80"
      - "443:443"
    networks:
      - postgrest-backend

networks:
  postgrest-backend:
    driver: bridge

and the test file :

import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.createSupabaseClient
import io.github.jan.supabase.postgrest.Postgrest
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.testcontainers.containers.ComposeContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.io.File

@Testcontainers
class MainKtTestTestContainers {

    // The jwt token is calculated manually (https://jwt.io/) based on the private key in the docker-compose.yml file, and a payload of {"role":"postgres"} to match the user in the database
    private val jwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXMifQ.88jCdmcEuy2McbdwKPmuazNRD-dyD65WYeKIONDXlxg"

    private lateinit var supabaseClient: SupabaseClient

    @Container
    var environment: ComposeContainer =
        ComposeContainer(File("src/test/resources/docker-compose.yml"))
            .withExposedService("postgrest-db", 5432)
            .withExposedService("postgrest", 3000)
            .withExposedService("nginx", 80)

    @BeforeEach
    fun setUp() {
        val fakeSupabaseUrl = environment.getServiceHost("nginx", 80) +
                ":" + environment.getServicePort("nginx", 80)

        supabaseClient = createSupabaseClient(
            supabaseUrl = "http://$fakeSupabaseUrl",
            supabaseKey = jwtToken
        ) {
            install(Postgrest)
        }
    }

    @Test
    fun testEmptyPersonTable(){
        runBlocking {
            val result = getPerson(supabaseClient)
            assertEquals(0, result.size)
        }
    }

    @Test
    fun testSavePersonAndRetrieve(){
        val randomPersons = listOf(Person("Jan", 30), Person("Jane", 42))

        runBlocking {
            val result = savePerson(randomPersons, supabaseClient)
            assertEquals(2, result.size)
            assertEquals(randomPersons, result.map { it.toPerson() })

            val fetchResult = getPerson(supabaseClient)
            assertEquals(2, fetchResult.size)
            assertEquals(randomPersons, fetchResult.map { it.toPerson() })
        }
    }
}