임의의 코드 테스트하기

임의의 코드 테스트하기

Lincheck는 임의의 동시성 코드를 테스트할 수 있는 runConcurrentTest() 함수를 제공해요. runConcurrentTest() 함수는 동시성 코드 블록을 여러 번 실행하고, 모델 체킹(model checking)을 사용해 가능한 실행 스케줄을 탐색해요.

출처: Testing arbitrary code

본문

Lincheck로 동시성 코드를 테스트하려면:

  1. 테스트 클래스를 만들어요:
class CounterTestWithInvocations {
}
  1. runConcurrentTest()를 사용해 테스트 함수를 멤버 함수로 만들어요:
@Test
fun test() = Lincheck.runConcurrentTest(100_000) {
    var counter = 0

    // Increments the counter concurrently
    val t1 = thread { counter++ }
    val t2 = thread { counter++ }

    // Waits for the threads to finish
    t1.join()
    t2.join()

    // Checks that both increments have been applied
    assertEquals(2, counter)
}

함수 매개변수는 선택 사항이며, 탐색할 실행 스케줄의 수를 지정해요. 기본값은 10_000이에요.

  1. 테스트를 실행해요. 실패하면 Lincheck는 잘못된 동작으로 이어지는 실행 스케줄이 담긴 보고서를 생성해요:
| ------------------------------------------------------------------------------- |
|                   Main Thread                   |   Thread 1    |   Thread 2    |
| ------------------------------------------------------------------------------- |
| thread(block = Lambda#2): Thread#1              |               |               |
| thread(block = Lambda#3): Thread#2              |               |               |
| switch (reason: waiting for Thread 1 to finish) |               |               |
|                                                 |               | run()         |
|                                                 |               |   counter ➜ 0 |
|                                                 |               |   switch      |
|                                                 | run()         |               |
|                                                 |   counter ➜ 0 |               |
|                                                 |   counter = 1 |               |
|                                                 |               |   counter = 1 |
| Thread#1.join()                                 |               |               |
| Thread#2.join()                                 |               |               |
| counter.element ➜ 1                             |               |               |
| assertEquals(2, 1): threw AssertionFailedError  |               |               |
| ------------------------------------------------------------------------------- |

예: ConcurrentHashMap 함수 테스트하기

ConcurrentHashMap 함수를 위한 다음 테스트를 살펴봐요:

package org.lincheck.docs

import org.jetbrains.lincheck.*
import kotlin.test.Test
import java.util.concurrent.*
import kotlin.concurrent.*

// This test demonstrates a deadlock caused by two threads
// performing nested `computeIfAbsent` calls in opposite order.
class ConcurrentHashMapDeadlockTest {
    @Test
    fun test() = Lincheck.runConcurrentTest {
        val map = ConcurrentHashMap<String, String>()

        // Updates `key2` while locking `key1`.
        val thread1 = thread {
            map.computeIfAbsent("key1") {
                map.computeIfAbsent("key2") { "value2" }
                "value1"
            }
        }
        
        // Updates `key1` while locking `key2`.
        val thread2 = thread {
            map.computeIfAbsent("key2") {
                map.computeIfAbsent("key1") { "value1" }
                "value2"
            }
        }

        // Wait until both threads complete.
        thread1.join()
        thread2.join()
    }
}

이 테스트는 교착 상태(deadlock)로 이어지는 실행 스케줄을 Lincheck가 찾아내서 실패해요:

  1. Thread 2는 key2를 인덱스 1의 버킷에 매핑하고, 이 버킷에 락을 걸고 computeIfAbsent("key1") 실행을 시작해요. Thread 2가 key1을 매핑하고 key1이 있는 버킷을 잠그기 전에 실행이 Thread 2에서 Thread 1로 전환돼요.
  2. Thread 1은 key1을 인덱스 0의 버킷에 매핑하고, 이 버킷에 락을 걸고 computeIfAbsent("key2") 실행을 시작해요. Thread 1은 key2를 인덱스 1의 버킷에 매핑하고 그 버킷을 잠그려 하지만, 이미 Thread 2가 잠가 놓았어요. 실행이 Thread 1에서 Thread 2로 전환돼요.
  3. Thread 2는 key1이 있는 버킷을 잠그려 하지만, 이미 Thread 1이 잠가 놓았어요.

두 스레드 모두 잠겨 있으므로, 실행은 교착 상태를 만나게 돼요.

다음 단계

Lincheck를 사용해 데이터 구조를 테스트하는 방법을 알아보세요.

더 알아보기

  • Lincheck의 모델 체킹 (Model checking in Lincheck)
  • Kotlin Multiplatform 프로젝트에서의 Lincheck (Lincheck in Kotlin Multiplatform projects)