Рубрики
Без рубрики

Kotlin – Модульное Тестирование Классов Без Выхода Из Общедоступного API!

Введение Kotlin – это удивительный новый перспективный язык. Он очень активно развивается… С тегами kotlin, jvm, java, тестирование.

Kotlin – это удивительный новый перспективный язык. Он очень активно разрабатывается и имеет массу функций, которые делают его очень привлекательным. Он неуклонно набирает долю рынка с тех пор, как Google добавил для него поддержку разработки Android (еще в 2017 году) и сделали его предпочтительным языком для такой разработки ровно год назад (май 2019 года)

Для любого разработчика Java, переходящего на этот язык, предупреждение о спойлере, вас ждет один большой сюрприз – модификатор видимости package-private отсутствует. Его не существует.

Модификатор доступа – это способ установить доступность класса|метода|переменной в объектно-ориентированных языках. Это играет важную роль в облегчении правильной инкапсуляции компонентов .

Ява

В Java у нас есть четыре модификатора доступа для любых методов внутри класса:

  • void doSomething() – модификатор по умолчанию, мы называем этот пакет-private. Любые такие объявления видны только в пределах одного и того же пакета (отсюда и название, private для пакета)
  • private void doSomething() — частный модификатор, объявления которого видны только внутри того же класса
  • защищенный void doSomething () — объявления видны только внутри пакета или всех подклассов.
  • public void doSomething() — объявления видны везде.

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

package doer.something;

/**
 * Does something.
 */
public class JSomethingDoer {

  private int motivationLevel;

  /**
   * @param motivationLevel a 0-100 integer, denoting how motivated the #{@link JSomethingDoer} is.
   */
  public JSomethingDoer(int motivationLevel) {
    this.motivationLevel = motivationLevel;
  }

  public void doSomething() {
    doOneLittleThing();
    maybeDoSecondLittleThing();
  }

  void doOneLittleThing() {
    System.out.println("Doing one thing.");
  }

  // package-private for testing
  void maybeDoSecondLittleThing() {
    if (feelingAmbitious()) {
      doSecondLittleThing();
    }
  }

  private void doSecondLittleThing() {
    System.out.println("We're overachievers, doing a second thing!!!");
  }

  private boolean feelingAmbitious() {
    return motivationLevel > 90;
  }
}

Мы можем непосредственно протестировать только три из пяти описанных выше методов. Поскольку do Second Little Thing() и feeling Ambitious()/| являются частными, наши модульные тесты буквально не могут вызывать эти методы, следовательно, мы не можем тестировать их напрямую.

Единственный способ протестировать их – это использовать метод package-private может быть, сделать Вторую маленькую вещь () , которую можно вызвать в наших модульных тестах, потому что они находятся в том же пакете, что и тестируемый класс.

package doer.something;

import org.junit.Before;
import org.junit.Test;

public class JSomethingDoerTest {

  private JSomethingDoer doer;

  @Before
  public void setUp() {
    doer = new JSomethingDoer(11);
  }

  @Test
  public void Test_doSomething() {
    doer.doSomething();
    // assert
  }

  @Test
  public void Test_doOneLittleThing() {
    doer.doOneLittleThing();
    // assert
  }

  @Test
  public void Test_maybeDoSecondLittleThing() {
    doer.maybeDoSecondLittleThing();
    // assert
  }
}

Модификатор package-private очень полезен в этом примере, поскольку он позволяет нам протестировать может быть, сделать Вторую маленькую вещь() метод напрямую! Тестирование было бы более громоздким, если бы нам пришлось тестировать все пути к коду может быть, сделать Вторую маленькую вещь() метод, вызвав public doSomething() метод.

Модификатор package-private также гарантирует, что мы не будем передавать какие-либо такие внутренние методы в пакеты за пределами этого.

Обратите внимание на другую упаковку деятель в Основном классе.

Котлин

Kotlin немного более странный в этом отношении. Он также имеет четыре модификатора:

  • private fun doSomething() — ванильный приватный модификатор
  • защищенный fun doSomething() — такой же, как защищенный Java — видимый только в этом классе и подклассах.
  • публичное развлечение Что-то делает( )

Эти три довольно стандартны. Я хочу сосредоточиться на четвертом:

  • внутренняя функция doSomething() — Внутренние объявления видны в любом месте внутри того же модуля .

В этом смысле это так же хорошо, как и общедоступно внутри одного и того же модуля. Многие небольшие проекты не нуждаются в нескольких модулях, но они, вероятно, будут иметь несколько пакетов.

В принципе, нет никакого способа ограничить доступ к методу Kotlin только внутри одного пакета.

Что это значит? Это означает, что нам нужно либо сделать наши методы закрытыми, что усложнит наше тестирование, либо сделать их общедоступными, тем самым предоставляя ненужный API компонентам в том же модуле.

Наш тот же пример, приведенный выше, снова в пакете doer.something :

package doer.something

/**
 * Does something.
 * @param motivationLevel a 0-100 integer, denoting how motivated the #[SomethingDoer] is.
 */
open class SomethingDoer(private val motivationLevel: Int) {
  fun doSomething() {
    doOneLittleThing()
    maybeDoSecondLittleThing()
  }

  protected fun doOneLittleThing() {
    println("Doing one thing.")
  }

  private fun maybeDoSecondLittleThing() {
    if (feelingAmbitious()) {
      doSecondLittleThing()
    }
  }

  private fun doSecondLittleThing() {
    println("We're overachievers, doing a second thing!!!")
  }

  private fun feelingAmbitious(): Boolean {
    return motivationLevel > 90
  }
}

Но на этот раз из пакета doer мы можем вызвать любой метод:

Решение 1 — Рефакторинг!

Похоже, принято считать , что мы не должны тестировать методы, которые являются/должны быть частными. Идея заключается в том, что:

  • все частные методы доступны с помощью общедоступных методов
  • общедоступные методы предоставляют интерфейс/контракт класса
  • частные методы – это детали реализации, и мы хотим протестировать функциональность класса, а не реализация

Хотя в теории это имеет смысл, реальность не такая черно-белая.

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

Однако в некоторых случаях более чистым и удобным в обслуживании является получение полного тестового покрытия путем тщательного тестирования каждого крошечного кода частного метода, вместо того, чтобы тестировать каждую условную ветвь с помощью общедоступных методов или создавать ненужные шаблоны (например, дополнительные классы).

Решение 2 — Отражение

Альтернативным решением является сохранение методов private и использование функций отражения языка для вызова указанных частных методов.

Добавьте зависимость от библиотеки kotlin-reflect и выбросьте код!

  fun callPrivate(objectInstance: Any, methodName: String, vararg args: Any?): Any? {
    val privateMethod: KFunction<*>? =
        objectInstance::class.members.find { t -> return@find t.name == methodName } as KFunction<*>?

    val argList = args.toMutableList()
    (argList as ArrayList).add(0, objectInstance)
    val argArr = argList.toArray()

    if (privateMethod?.javaMethod?.trySetAccessible()!!) {
      privateMethod?.apply {
        return call(*argArr)
      } ?: throw NoSuchMethodException("Method $methodName does not exist in ${objectInstance::class.qualifiedName}")
    } else throw IllegalAccessException("Method $methodName could not be made accessible")

    return null
  }

Вау.

Здесь наш call Private() помощник по тестированию – звезда шоу! Это позволяет нам вызывать частные методы, хотя и необычным способом. Нам нужно указать имя метода строкой, следовательно, мы теряем все гарантии проверки типов. Это приводит к хрупкому коду, который также трудно читать.

Мы можем использовать плагин компилятора |/, например Manifold , чтобы получить типобезопасное метапрограммирование. Но вы можете возразить, что это заходит слишком далеко, поскольку вводит большую зависимость, которая включает в себя гораздо больше метапрограммирования за кулисами.

Решение 3 — Откройте класс, защитите методы и протестируйте оболочку!

Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами.

Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами. Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами.

open class SomethingDoer(private val motivationLevel: Int) {
  fun doSomething() {
    doOneLittleThing()
    maybeDoSecondLittleThing()
  }

  protected fun doOneLittleThing() {
    println("Doing one thing.")
  }
}

Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами.

class SomethingDoerTest {

  class TestSomethingDoer(motivationLevel: Int) : SomethingDoer(motivationLevel) {
    fun testDoOneLittleThing() {
      super.doOneLittleThing()
    }
  }
  private lateinit var doer: SomethingDoer
  private lateinit var doerWrapper: TestSomethingDoer

  @Before
  fun setUp() {
    doer = SomethingDoer(99)
    doerWrapper = TestSomethingDoer(99)
  }

  @Test
  fun doSomething() {
    doer.doSomething()
    // assert
  }

  @Test
  fun Test_doOneLittleThing() {
    doerWrapper.testDoOneLittleThing()
    // assert
  }
}

Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами.

Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами.

Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами. Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами.

Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами.

Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами.

Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами. Третий, возможно, предпочтительный способ решить эту проблему – немного поработать над подклассами.

Оригинал: “https://dev.to/enether/kotlin-unit-testing-classes-without-leaking-public-api-5dp9”