Spec-Zone.ru › Kotlin 2

Как тестировать структуры данных

Lincheck предоставляет декларативный интерфейс для тестирования конкурентных структур данных. Вместо того чтобы описывать, как выполнять тест, вы объявляете все операции, которые нужно проверить, а Lincheck генерирует сценарии конкурентного выполнения, запускает их и анализирует результаты.

Протестируем с помощью Lincheck эту структуру данных Counter:

class Counter {
    var value = 0

    fun inc(): Int = ++value
    fun dec(): Int = --value
}
  1. Создайте тестовый класс:

    class CounterTest {
    }
    
  2. Создайте свойство класса, которое будет хранить экземпляр вашей структуры:

    private val c = Counter()
    
  3. Объявите операции, которые хотите проверить, в виде функций-членов и пометьте их аннотацией @Operation:

        @Operation
        fun inc() = c.inc()
    
        @Operation
        fun dec() = c.dec()
    

    Эта аннотация указывает Lincheck, какие методы включать при генерации сценариев выполнения.

  4. Объявите тестовую функцию в виде функции-члена с помощью ModelCheckingOptions() или StressOptions(). Пометьте её аннотацией @Test:

        @Test
        fun test() = ModelCheckingOptions().check(this::class)
    

    Узнайте о различиях между проверкой модели и стресс-тестированием в статье «Стратегии тестирования».

  5. Запустите тест. Если он завершится с ошибкой, Lincheck сформирует отчет об ошибке со сценарием и трассировкой выполнения, которые привели к некорректному поведению:

    = Invalid execution results =
    | -------------------- |
    | Thread 1  | Thread 2 |
    | -------------------- |
    | dec(): -1 | inc(): 1 |
    | -------------------- |
    

Процесс тестирования

При тестировании структуры данных Lincheck генерирует список сценариев выполнения, запускает их и анализирует результаты.

Рассмотрим эту структуру данных Counter:

A diagram of a Counter data structure with two methods: inc() and dec()

Чтобы протестировать её, Lincheck выполняет следующие шаги:

  1. Генерирует список случайных сценариев выполнения, случайным образом распределяя объявленные операции по разным потокам:

    A diagram of four execution scenarios. In each scenario, operations are placed in different orders 
in two threads.

    Количество потоков и операций в каждом потоке можно задать с помощью параметров конфигурации, предоставляемых Lincheck.

  2. Выполняет сгенерированные сценарии с использованием выбранной стратегии тестирования: проверки модели или стресс-тестирования. Каждый сгенерированный сценарий выполняется несколько раз, чтобы исследовать различные расписания выполнения:

    A diagram of four execution schedules. All schedules correspond to a single execution scenario.
In each schedule, the operations interrupt each other at different times.
  3. Проверяет результаты выполнения на соответствие свойству корректности. По умолчанию это линеаризуемость.

    A diagram of the verification process. The results of one execution schedule are compared
to the results of the same operations executed sequentially.

    На этом шаге Lincheck также может проверить структуру, если ему передана функция проверки.

Пример: тестирование реализации стека Трайбера

Рассмотрим эту некорректную реализацию стека Трайбера:

class TreiberStack<E> {
    private val top = AtomicReference<Node<E>?>(null)

    fun push(item: E) {
        val newHead = Node(item)
        var oldHead: Node<E>?

        do {
            oldHead = top.get()
            newHead.next = oldHead
        } while (!top.compareAndSet(oldHead, newHead))
    }

    fun pop(): E? {
        val oldHead = top.get()

        if (oldHead == null) {
            return null
        }

        val newHead = oldHead.next
        top.compareAndSet(oldHead, newHead)

        // Bug: by the time `pop()` finishes execution,
        // another thread might have already popped this item.
        return oldHead.item
    }

    private class Node<E>(
        val item: E,
        var next: Node<E>? = null
    )
}

С помощью Lincheck можно протестировать эту структуру и изучить, как внесенная ошибка влияет на поведение программы:

  1. Создайте тестовую структуру:

    class TreiberStackTest {
        private val stack = TreiberStack<Int>()
    
        @Operation
        fun push(value: Int) = stack.push(value)
    
        @Operation
        fun pop(): Int? = stack.pop()
    
        @Test
        fun modelCheckingTest() = ModelCheckingOptions()
            .check(this::class)
    }
    
  2. Запустите тест. Lincheck сформирует отчет об ошибке и укажет сценарий выполнения, который приводит к некорректному поведению:

    | ------------------------------ |
    |   Thread 1    |    Thread 2    |
    | ------------------------------ |
    | push(1): void |                |
    | ------------------------------ |
    | pop(): 1      | push(-1): void |
    | ------------------------------ |
    | pop(): -1     |                |
    | pop(): 1      |                |
    | ------------------------------ |
    

    На этой диаграмме показано, как операции распределены по разным потокам, а также значения, возвращаемые операциями. Lincheck также показывает конкретное чередование потоков, приводящее к некорректным результатам:

    | ----------------------------------------------------- |
    |                  Thread 1                  | Thread 2 |
    | ----------------------------------------------------- |
    | push(1)                                    |          |
    | ----------------------------------------------------- |
    | pop(): 1                                   |          |
    |   stack.pop(): 1                           |          |
    |     top.get(): Node#1                      |          |
    |     switch                                 |          |
    |                                            | push(-1) |
    |     oldHead.getNext(): null                |          |
    |     top.compareAndSet(Node#1, null): false |          |
    |     oldHead.getItem(): 1                   |          |
    |   result: 1                                |          |
    | ----------------------------------------------------- |
    | pop(): -1                                  |          |
    | pop(): 1                                   |          |
    | ----------------------------------------------------- |
    

    Поскольку реализация не учитывает возможность прерывания функции pop() другим потоком, pop() возвращает 1 дважды, чего происходить не должно.

  3. Исправьте структуру данных. Корректная реализация обновляет переменную oldHead до последнего значения перед возвратом результатов:

        fun pop(): E? {
            var oldHead: Node<E>?
            var newHead: Node<E>?
    
            do {
                oldHead = top.get()
                if (oldHead == null) return null
                newHead = oldHead.next
            } while (!top.compareAndSet(oldHead, newHead))
    
            return oldHead.item
         }
    

Что дальше

Узнайте о стратегиях тестирования, доступных в Lincheck.

См. также

  • Генерация аргументов операций

  • Настройка параметров выполнения операций

  • Проверка гарантий неблокирующего прогресса

  • Определение последовательной спецификации алгоритма

15 августа 2026 г.
Тестирование произвольного кодаСтратегии тестирования

© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/lincheck-how-to-test-data-structures.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API