Search code examples
kotlinunit-testingsupabasesupabase-database

Mocking the Kotlin SupabaseClient for unit testing


TLDR : Here is a minimally reproduceable example. You do not need a Supabase account / database to get the error.

I have some code like this, which performs some simple transformation of data before saving it in Supabase

    suspend fun saveClones(owner: String, repository: String, clones: GitHubClones): List<ResultClone> {
        val zeClones = clones.clones.map {
            Clone(
                owner = owner,
                repository = repository,
                count = it.count,
                uniques = it.uniques,
                timestamp = it.timestamp
            )
        }

        return client.postgrest["clone"].insert(zeClones, upsert = true, onConflict = "owner, repository, timestamp").decodeList<ResultClone>()
    }

I'd like to check that the data going in the insert call is what I expect it to be. I've tried to mock the client using mockk as such

val supabaseClient = mockk<SupabaseClient>()
val postgrest = mockk<Postgrest>()
val postgrestBuilder = mockk<PostgrestBuilder>()
val postgrestResult = PostgrestResult(body = null, headers = Headers.Empty)

every { supabaseClient.postgrest } returns postgrest
every { postgrest["path"] } returns postgrestBuilder
coEvery { postgrestBuilder.insert(values = any<List<Path>>()) } returns postgrestResult

but when running my test, I am facing the following error :

java.lang.IllegalStateException: Plugin rest not installed or not of type Postgrest. Consider installing Postgrest within your supabase client builder
    at io.github.jan.supabase.postgrest.PostgrestKt.getPostgrest(Postgrest.kt:172)
    at nl.lengrand.GitHubTrafficStoreTest$setUp$1.invoke(GitHubTrafficStoreTest.kt:53)
    at nl.lengrand.GitHubTrafficStoreTest$setUp$1.invoke(GitHubTrafficStoreTest.kt:53)

What would be the correct way to unit test that function? It looks like I also cannot really use test containers with Supabase, making integration tests equally as difficult.


Solution

  • 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() })
            }
        }
    }