Tout ce dont vous avez besoin pour commencer est simplement d'ajouter une dépendance à la bibliothèque MockK .
| Approche | Instruction |
|---|---|
![]() | Témoignage "io.mockk: mockk: $ {mockkversion}"
|
(Kotlin DSL) | Témoignage ("io.mockk: mockk: $ {mockkversion}") |
![]() | <dépendance>
<bandrandid> io.mockk </romproudID>
<ArtefactId> mockk-jvm </refactive>
<version> $ {MockKversion} </DERNIER>
<ccope> Test </ccope>
</Dependance>
|
Témoignage "io.mockk: mockk-android: $ {mockkversion}"
Témoignage "io.mockk: mockk-agent: $ {mockkversion}"
| |
AndroidStimplementation "io.mockk: mockk-android: $ {mockkversion}"
AndroidTestimplementation "io.mockk: mockk-agent: $ {mockkversion}"
|
Exemple le plus simple. Par défaut, les maquettes sont strictes, vous devez donc fournir un comportement.
val car = mockk< Car >()
every { car.drive( Direction . NORTH ) } returns Outcome . OK
car.drive( Direction . NORTH ) // returns OK
verify { car.drive( Direction . NORTH ) }
confirmVerified(car)Voir la section "Fonctionnalités" ci-dessous pour des exemples plus détaillés.
De la version 1.13.0 Mockk prend en charge Kotlin 1.4 et plus
mockkStatic peut ne pas fonctionner sur JDK 16+; InaccessibleObjectException / IllegalAccessException : Lire la suite iciTable des matières:
Vérifiez la série d'articles "Mocking Is Not Rocket Science" à KT. Academy décrivant Mockk des bases mêmes de se moquer de la description de toutes les fonctionnalités avancées.
Bases
Comportement et vérification du comportement attendus
Fonctionnalités Mockk
Fonctionnalités avancées de Mockk
Tester Quarkus avec Kotlin, Junit et Mockk
Déstaurer la magie noire de Mockk (en, traduction)
Guide Mockk
«Kotlin Unit Testing with Mockk» de Marco Cattaneo
(Vidéo) Utilisez Vérifier dans MockK pour valider les appels de fonction sur un objet moque
Test avec un cours payant Mockk sur Raywenderlich.com
TDD pour Android Video Tutorial Part 1, partie 2 par Ryan Kay
(Vidéo) Développeur Android Codage en direct # 13: tests unitaires avec Mockk, Coroutines, développement piloté aux tests
KotlinConf 2018 - Best Practices for Unit Test in Kotlin par Philipp Hauer
Kotlin-Fullstack-échantillon utilise un projet Mockk couvert de tests
Article Dzone
Article Habrahabr (RU)
Se moquant à Kotlin avec Mockk - Yannick de Turck
Vous pouvez utiliser des annotations pour simplifier la création d'objets simulés:
class TrafficSystem {
lateinit var car1 : Car
lateinit var car2 : Car
lateinit var car3 : Car
}
class CarTest {
@MockK
lateinit var car1 : Car
@RelaxedMockK
lateinit var car2 : Car
@MockK(relaxUnitFun = true )
lateinit var car3 : Car
@SpyK
var car4 = Car ()
@InjectMockKs
var trafficSystem = TrafficSystem ()
@Before
fun setUp () = MockKAnnotations . init ( this , relaxUnitFun = true ) // turn relaxUnitFun on for all mocks
@Test
fun calculateAddsValues1 () {
// ... use car1, car2, car3 and car4
}
} L'injection essaie d'abord de faire correspondre les propriétés par son nom, puis par classe ou superclass. Vérifiez le paramètre lookupType pour la personnalisation.
Les propriétés sont injectées même si private est appliqué. Les constructeurs d'injection sont sélectionnés parmi le plus grand nombre d'arguments au plus bas.
@InjectMockKs par défaut n'injects que lateinit var s ou var s qui ne sont pas affectés. Pour changer cela, utilisez overrideValues = true . Cela attribuerait la valeur même s'il est déjà initialisé d'une manière ou d'une autre. Pour injecter val s, utilisez injectImmutable = true . Pour une notation plus courte, utilisez @OverrideMockKs qui fait de même que @InjectMockKs par défaut, mais allume ces deux drapeaux.
JUnit 4 expose une API basée sur des règles pour permettre une certaine automatisation après le cycle de vie du test. Mockk comprend une règle qui utilise ceci pour installer et démolir vos simulations sans avoir besoin d'appeler manuellement MockKAnnotations.init(this) . Exemple:
class CarTest {
@get:Rule
val mockkRule = MockKRule ( this )
@MockK
lateinit var car1 : Car
@RelaxedMockK
lateinit var car2 : Car
@Test
fun something () {
every { car1.drive() } just runs
every { car2.changeGear(any()) } returns true
// etc
}
} Dans JUnit5, vous pouvez utiliser MockKExtension pour initialiser vos simulations.
@ExtendWith( MockKExtension :: class )
class CarTest {
@MockK
lateinit var car1 : Car
@RelaxedMockK
lateinit var car2 : Car
@MockK(relaxUnitFun = true )
lateinit var car3 : Car
@SpyK
var car4 = Car ()
@Test
fun calculateAddsValues1 () {
// ... use car1, car2, car3 and car4
}
} De plus, il ajoute la possibilité d'utiliser @MockK et @RelaxedMockK sur les paramètres de fonction de test:
@Test
fun calculateAddsValues1 (@MockK car1 : Car , @RelaxedMockK car2 : Car ) {
// ... use car1 and car2
} Enfin, cette extension appellera unmockkAll et clearAllMocks dans un rappel @AfterAll , garantissant que votre environnement de test est propre après chaque exécution de classe de test. Vous pouvez désactiver ce comportement en ajoutant l'annotation @MockKExtension.KeepMocks à votre classe ou à l'échelle mondiale en définissant la propriété mockk.junit.extension.keepmocks=true Property. (Depuis V1.13.11) Alternativement, puisque clearAllMocks par défaut ( currentThreadOnly=false ) n'est pas fileté, si vous avez besoin d'exécuter un test en parallèle, vous pouvez ajouter le MockKExtension.RequireParallelTesting @AfterAll mockk.junit.extension.requireParallelTesting=true . Si clearAllMocks est explicitement appelé, vous pouvez fournir clearAllMocks(currentThreadOnly = true) afin qu'il efface uniquement les simulations créées dans le même thread (puisque V1.13.12).
Vous pouvez vous assurer que toutes les méthodes Stubbed sont effectivement vérifiées en annotant également votre classe de test avec @MockKExtension.ConfirmVerification .
Cela appellera en interne confirmVerified sur toutes les simulations après chaque test, pour s'assurer qu'il n'y a pas de bobs inutiles.
Veuillez noter que ce comportement peut ne pas fonctionner comme prévu lors de l'exécution de tests dans votre IDE, car c'est Gradle qui s'occupe de la gestion de l'exception lancée lorsque ces appels confirmVerified échouent.
Vous pouvez vous assurer que toutes les méthodes Stubbed sont utiles - utilisées au moins une fois - en annotant également votre classe de test avec @MockKExtension.CheckUnnecessaryStub .
Cela appellera en interne checkUnnecessaryStub sur toutes les simulations après chaque test, pour s'assurer qu'il n'y a pas de bobs inutiles.
Les espions vous permettent de mélanger des simulations et des objets réels.
val car = spyk( Car ()) // or spyk<Car>() to call the default constructor
car.drive( Direction . NORTH ) // returns whatever the real function of Car returns
verify { car.drive( Direction . NORTH ) }
confirmVerified(car)Remarque 1: L'objet d'espion est une copie de l'objet passé. Remarque 2: Il y a un problème connu si vous utilisez un espion avec une fonction de suspension: # 554
Une relaxed mock est la simulation qui renvoie une valeur simple pour toutes les fonctions. Cela vous permet de sauter le comportement de spécification de chaque cas, tout en couchant des choses dont vous avez besoin. Pour les types de référence, des simulations enchaînées sont retournées.
val car = mockk< Car >(relaxed = true )
car.drive( Direction . NORTH ) // returns null
verify { car.drive( Direction . NORTH ) }
confirmVerified(car)REMARQUE: Mocking Reladed fonctionne mal avec les types de retour génériques. Une exception de casting de classe est généralement lancée dans ce cas. Optez pour les coups manuellement dans le cas d'un type de retour générique.
Solution:
val func = mockk < () -> Car > (relaxed = true ) // in this case invoke function has generic return type
// this line is workaround, without it the relaxed mock would throw a class cast exception on the next line
every { func() } returns Car () // or you can return mockk() for example
func() Parfois, vous devez coller certaines fonctions, mais appeler la méthode réelle sur d'autres ou sur des arguments spécifiques. Ceci est possible en passant callOriginal() aux answers , qui fonctionnent à la fois pour les simulations détendues et non relaxées.
class Adder {
fun addOne ( num : Int ) = num + 1
}
val adder = mockk< Adder >()
every { adder.addOne(any()) } returns - 1
every { adder.addOne( 3 ) } answers { callOriginal() }
assertEquals( - 1 , adder.addOne( 2 ))
assertEquals( 4 , adder.addOne( 3 )) // original function is called Si vous voulez que les fonctions de retour Unit soient détentes, vous pouvez utiliser relaxUnitFun = true comme argument de la fonction mockk , de la fonction @MockK annotation ou MockKAnnotations.init .
Fonction:
mockk< ClassBeingMocked >(relaxUnitFun = true )Annotation:
@MockK(relaxUnitFun = true )
lateinit var mock1 : ClassBeingMocked
init {
MockKAnnotations . init ( this )
}MockKannotations.Init:
@MockK
lateinit var mock2 : ClassBeingMocked
init {
MockKAnnotations . init ( this , relaxUnitFun = true )
}Les objets peuvent être transformés en simulations de la manière suivante:
object ObjBeingMocked {
fun add ( a : Int , b : Int ) = a + b
}
mockkObject( ObjBeingMocked ) // applies mocking to an Object
assertEquals( 3 , ObjBeingMocked .add( 1 , 2 ))
every { ObjBeingMocked .add( 1 , 2 ) } returns 55
assertEquals( 55 , ObjBeingMocked .add( 1 , 2 )) Pour revenir en arrière, utilisez unmockkObject ou unmockkAll (plus destructeur: annule l'objet, les simulations statiques et constructeurs)
@Before
fun beforeTests () {
mockkObject( ObjBeingMocked )
every { ObjBeingMocked .add( 1 , 2 ) } returns 55
}
@Test
fun willUseMockBehaviour () {
assertEquals( 55 , ObjBeingMocked .add( 1 , 2 ))
}
@After
fun afterTests () {
unmockkObject( ObjBeingMocked )
// or unmockkAll()
}Malgré les restrictions de la langue Kotlin, vous pouvez créer de nouvelles instances d'objets si elles sont nécessaires en testant la logique:
val newObjectMock = mockk< ObjBeingMocked >() Parfois, vous avez besoin d'une maquette d'une classe arbitraire. Utilisez mockkClass dans ces cas.
val car = mockkClass( Car :: class )
every { car.drive( Direction . NORTH ) } returns Outcome . OK
car.drive( Direction . NORTH ) // returns OK
verify { car.drive( Direction . NORTH ) } Les énumérations peuvent être moquées à l'aide de mockkObject :
enum class Enumeration ( val goodInt : Int ) {
CONSTANT ( 35 ),
OTHER_CONSTANT ( 45 );
}
mockkObject( Enumeration . CONSTANT )
every { Enumeration . CONSTANT .goodInt } returns 42
assertEquals( 42 , Enumeration . CONSTANT .goodInt)Parfois, surtout dans le code que vous ne possédez pas, vous devez vous moquer d'objets nouvellement créés. À cette fin, les constructions suivantes sont fournies:
class MockCls {
fun add ( a : Int , b : Int ) = a + b
}
mockkConstructor( MockCls :: class )
every { anyConstructed< MockCls >().add( 1 , 2 ) } returns 4
assertEquals( 4 , MockCls ().add( 1 , 2 )) // note new object is created
verify { anyConstructed< MockCls >().add( 1 , 2 ) } L'idée de base est que juste après l'exécution du constructeur de la classe moquette (chacun d'entre eux), les objets deviennent une constructed mock .
Le comportement moqueur d'une telle simulation est connecté au prototype mock indiquée par anyConstructed<MockCls>() .
Il y a une instance par classe d'un tel prototype mock . L'enregistrement des appels arrive également au prototype mock .
Si aucun comportement pour la fonction n'est spécifié, la fonction d'origine est exécutée.
Dans le cas où une classe a plus d'un constructeur, chacune peut être moquée séparément:
class MockCls ( private val a : Int = 0 ) {
constructor (x : String ) : this (x.toInt())
fun add ( b : Int ) = a + b
}
mockkConstructor( MockCls :: class )
every { constructedWith< MockCls >().add( 1 ) } returns 2
every {
constructedWith< MockCls >( OfTypeMatcher < String >( String :: class )).add( 2 ) // Mocks the constructor which takes a String
} returns 3
every {
constructedWith< MockCls >( EqMatcher ( 4 )).add(any()) // Mocks the constructor which takes an Int
} returns 4
assertEquals( 2 , MockCls ().add( 1 ))
assertEquals( 3 , MockCls ( " 2 " ).add( 2 ))
assertEquals( 4 , MockCls ( 4 ).add( 7 ))
verify {
constructedWith< MockCls >().add( 1 )
constructedWith< MockCls >( " 2 " ).add( 2 )
constructedWith< MockCls >( EqMatcher ( 4 )).add( 7 )
} Notez que dans ce cas, un prototype mock est créé pour chaque ensemble d'arguments des matchs passés à constructedWith .
Vous pouvez mélanger à la fois des arguments et des matchs réguliers:
val car = mockk< Car >()
every {
car.recordTelemetry(
speed = more( 50 ),
direction = Direction . NORTH , // here eq() is used
lat = any(),
long = any()
)
} returns Outcome . RECORDED
car.recordTelemetry( 60 , Direction . NORTH , 51.1377382 , 17.0257142 )
verify { car.recordTelemetry( 60 , Direction . NORTH , 51.1377382 , 17.0257142 ) }
confirmVerified(car)Vous pouvez tacher des chaînes d'appels:
val car = mockk< Car >()
every { car.door( DoorType . FRONT_LEFT ).windowState() } returns WindowState . UP
car.door( DoorType . FRONT_LEFT ) // returns chained mock for Door
car.door( DoorType . FRONT_LEFT ).windowState() // returns WindowState.UP
verify { car.door( DoorType . FRONT_LEFT ).windowState() }
confirmVerified(car) Remarque: Si le type de retour de la fonction est générique, les informations sur le type réel ont disparu.
Pour faire fonctionner les appels enchaînés, des informations supplémentaires sont nécessaires.
La plupart du temps, le cadre attrapera l'exception de la distribution et fera autohinting .
Dans le cas, il est explicitement requis, utilisez hint avant de passer l'appel suivant.
every { obj.op2( 1 , 2 ).hint( Int :: class ).op1( 3 , 4 ) } returns 5De la version 1.9.1 Les simulations peuvent être enchaînées en hiérarchies:
interface AddressBook {
val contacts : List < Contact >
}
interface Contact {
val name : String
val telephone : String
val address : Address
}
interface Address {
val city : String
val zip : String
}
val addressBook = mockk< AddressBook > {
every { contacts } returns listOf (
mockk {
every { name } returns " John "
every { telephone } returns " 123-456-789 "
every { address.city } returns " New-York "
every { address.zip } returns " 123-45 "
},
mockk {
every { name } returns " Alex "
every { telephone } returns " 789-456-123 "
every { address } returns mockk {
every { city } returns " Wroclaw "
every { zip } returns " 543-21 "
}
}
)
} Vous pouvez capturer un argument à CapturingSlot ou MutableList de capture.
CapturingSlot est généralement créé via la méthode d'usine slot<T : Any?>() Et est possible pour capturer des types nullables et non nullables. MutableList est destiné à capturer plusieurs valeurs lors des tests.
enum class Direction { NORTH , SOUTH }
enum class RecordingOutcome { RECORDED }
enum class RoadType { HIGHWAY }
class Car {
fun recordTelemetry ( speed : Double , direction : Direction , roadType : RoadType ? ): RecordingOutcome {
TODO ( " not implement for showcase " )
}
}
val car = mockk< Car >()
// allow to capture parameter with non nullable type `Double`
val speedSlot = slot< Double >()
// allow to capture parameter with nullable type `RoadType`
val roadTypeSlot = slot< RoadType ?>()
val list = mutableListOf< Double >()
every {
car.recordTelemetry(
speed = capture(speedSlot), // makes mock match calls with any value for `speed` and record it in a slot
direction = Direction . NORTH , // makes mock and capturing only match calls with specific `direction`. Use `any()` to match calls with any `direction`
roadType = captureNullable(roadTypeSlot), // makes mock match calls with any value for `roadType` and record it in a slot
)
} answers {
println ( " Speed: ${speedSlot.captured} , roadType: ${roadTypeSlot.captured} " )
RecordingOutcome . RECORDED
}
every {
car.recordTelemetry(
speed = capture(list),
direction = Direction . SOUTH ,
roadType = captureNullable(roadTypeSlot),
)
} answers {
println ( " Speed: ${list} , roadType: ${roadTypeSlot.captured} " )
RecordingOutcome . RECORDED
}
car.recordTelemetry(speed = 15.0 , direction = Direction . NORTH , null ) // prints Speed: 15.0, roadType: null
car.recordTelemetry(speed = 16.0 , direction = Direction . SOUTH , RoadType . HIGHWAY ) // prints Speed: [16.0], roadType: HIGHWAY
verifyOrder {
car.recordTelemetry(speed = or ( 15.0 , 16.0 ), direction = any(), roadType = null )
car.recordTelemetry(speed = 16.0 , direction = any(), roadType = RoadType . HIGHWAY )
}
confirmVerified(car) Vous pouvez vérifier le nombre d'appels avec les paramètres atLeast , atMost ou exactly :
val car = mockk< Car >(relaxed = true )
car.accelerate(fromSpeed = 10 , toSpeed = 20 )
car.accelerate(fromSpeed = 10 , toSpeed = 30 )
car.accelerate(fromSpeed = 20 , toSpeed = 30 )
// all pass
verify(atLeast = 3 ) { car.accelerate(allAny()) }
verify(atMost = 2 ) { car.accelerate(fromSpeed = 10 , toSpeed = or ( 20 , 30 )) }
verify(exactly = 1 ) { car.accelerate(fromSpeed = 10 , toSpeed = 20 ) }
verify(exactly = 0 ) { car.accelerate(fromSpeed = 30 , toSpeed = 10 ) } // means no calls were performed
confirmVerified(car) Ou vous pouvez utiliser verifyCount :
val car = mockk< Car >(relaxed = true )
car.accelerate(fromSpeed = 10 , toSpeed = 20 )
car.accelerate(fromSpeed = 10 , toSpeed = 30 )
car.accelerate(fromSpeed = 20 , toSpeed = 30 )
// all pass
verifyCount {
( 3 .. 5 ) * { car.accelerate(allAny(), allAny()) } // same as verify(atLeast = 3, atMost = 5) { car.accelerate(allAny(), allAny()) }
1 * { car.accelerate(fromSpeed = 10 , toSpeed = 20 ) } // same as verify(exactly = 1) { car.accelerate(fromSpeed = 10, toSpeed = 20) }
0 * { car.accelerate(fromSpeed = 30 , toSpeed = 10 ) } // same as verify(exactly = 0) { car.accelerate(fromSpeed = 30, toSpeed = 10) }
}
confirmVerified(car)verifyAll vérifie que tous les appels se sont produits sans vérifier leur commande.verifySequence vérifie que les appels se sont produits dans une séquence spécifiée.verifyOrder vérifie que les appels se sont produits dans un ordre spécifique.wasNot Called vérifie que la simulation (ou la liste des simulations) n'a pas été appelée du tout. class MockedClass {
fun sum ( a : Int , b : Int ) = a + b
}
val obj = mockk< MockedClass >()
val slot = slot< Int >()
every {
obj.sum(any(), capture(slot))
} answers {
1 + firstArg< Int >() + slot.captured
}
obj.sum( 1 , 2 ) // returns 4
obj.sum( 1 , 3 ) // returns 5
obj.sum( 2 , 2 ) // returns 5
verifyAll {
obj.sum( 1 , 3 )
obj.sum( 1 , 2 )
obj.sum( 2 , 2 )
}
verifySequence {
obj.sum( 1 , 2 )
obj.sum( 1 , 3 )
obj.sum( 2 , 2 )
}
verifyOrder {
obj.sum( 1 , 2 )
obj.sum( 2 , 2 )
}
val obj2 = mockk< MockedClass >()
val obj3 = mockk< MockedClass >()
verify {
listOf (obj2, obj3) wasNot Called
}
confirmVerified(obj) Pour vérifier que tous les appels ont été vérifiés par verify... Constructs, vous pouvez utiliser confirmVerified :
confirmVerified(mock1, mock2) Il n'a pas beaucoup de sens de l'utiliser pour verifySequence et verifyAll , car ces méthodes de vérification couvrent déjà de manière exhaustive tous les appels avec vérification.
Il lèvera une exception s'il reste des appels sans vérification.
Certains appels peuvent être exclus de cette confirmation, vérifiez la section suivante pour plus de détails.
val car = mockk< Car >()
every { car.drive( Direction . NORTH ) } returns Outcome . OK
every { car.drive( Direction . SOUTH ) } returns Outcome . OK
car.drive( Direction . NORTH ) // returns OK
car.drive( Direction . SOUTH ) // returns OK
verify {
car.drive( Direction . SOUTH )
car.drive( Direction . NORTH )
}
confirmVerified(car) // makes sure all calls were covered with verificationÉtant donné que le code de test propre et maintenable ne nécessite aucun code inutile, vous pouvez vous assurer qu'il n'y a pas de talons inutiles.
checkUnnecessaryStub(mock1, mock2)Il lancera une exception s'il y a des appels déclarés sur les simulations qui ne sont pas utilisées par le code testé. Cela peut se produire si vous avez déclaré des talons vraiment inutiles ou si le code testé n'en appelle pas un.
Pour exclure l'enregistrement des appels sans importance, vous pouvez utiliser excludeRecords :
excludeRecords { mock.operation(any(), 5 ) } Tous les appels correspondants seront exclus de l'enregistrement. Cela peut être utile si vous utilisez une vérification exhaustive: verifyAll , verifySequence ou confirmVerified .
val car = mockk< Car >()
every { car.drive( Direction . NORTH ) } returns Outcome . OK
every { car.drive( Direction . SOUTH ) } returns Outcome . OK
excludeRecords { car.drive( Direction . SOUTH ) }
car.drive( Direction . NORTH ) // returns OK
car.drive( Direction . SOUTH ) // returns OK
verify {
car.drive( Direction . NORTH )
}
confirmVerified(car) // car.drive(Direction.SOUTH) was excluded, so confirmation is fine with only car.drive(Direction.NORTH) Pour vérifier les opérations simultanées, vous pouvez utiliser timeout = xxx :
mockk< MockCls > {
every { sum( 1 , 2 ) } returns 4
Thread {
Thread .sleep( 2000 )
sum( 1 , 2 )
}.start()
verify(timeout = 3000 ) { sum( 1 , 2 ) }
}Cela attendra l'un des deux états suivants: soit la vérification est passée ou le délai d'attente est atteint.
Si une fonction renvoie Unit , vous pouvez utiliser la construction justRun :
class MockedClass {
fun sum ( a : Int , b : Int ): Unit {
println (a + b)
}
}
val obj = mockk< MockedClass >()
justRun { obj.sum(any(), 3 ) }
obj.sum( 1 , 1 )
obj.sum( 1 , 2 )
obj.sum( 1 , 3 )
verify {
obj.sum( 1 , 1 )
obj.sum( 1 , 2 )
obj.sum( 1 , 3 )
} D'autres façons d'écrire justRun { obj.sum(any(), 3) } :
every { obj.sum(any(), 3) } just Runsevery { obj.sum(any(), 3) } returns Unitevery { obj.sum(any(), 3) } answers { Unit }Pour simuler les coroutines, vous devez ajouter une autre dépendance à la bibliothèque de support.
| Grade |
|---|
Témoignage "org.JetBrains.kotlinx: Kotlinx-Coroutines-Core: xx" |
| Maven |
|---|
<dépendance>
<GroupId> org.JetBrains.kotlinx </romproudID>
<ArtefactId> Kotlinx-Coroutines-Core </letefactid>
<version> xx </ version>
<ccope> Test </ccope>
</Dependance> |
Ensuite, vous pouvez utiliser coEvery , coVerify , coMatch , coAssert , coRun , coAnswers ou coInvoke to Mock Suspender Fonctions.
val car = mockk< Car >()
coEvery { car.drive( Direction . NORTH ) } returns Outcome . OK
car.drive( Direction . NORTH ) // returns OK
coVerify { car.drive( Direction . NORTH ) } Et pour simuler une fonction suspend sans retour, vous pouvez utiliser coJustAwait :
runTest {
val car = mockk< Car >()
coJustAwait { car.drive(any()) } // car.drive(...) will never return
val job = launch( UnconfinedTestDispatcher ()) {
car.drive( Direction . NORTH )
}
coVerify { car.drive( Direction . NORTH ) }
job.cancelAndJoin() // Don't forget to cancel the job
}Remarque: il y a un problème connu si vous utilisez un espion avec une fonction de suspension: # 554
Kotlin vous permet de déclarer des fonctions qui n'appartiennent à aucune classe ou objet, appelées fonctions de niveau supérieur. Ces appels sont traduits par des méthodes statiques dans des environnements jvm , et une classe Java spéciale est générée pour contenir les fonctions. Ces fonctions de niveau supérieur peuvent être moquées à l'aide de mockkStatic . Il vous suffit d'importer la fonction et de passer une référence comme argument:
import com.cars.buildCar
val testCar = Car ()
mockkStatic(::buildCar)
every { buildCar() } returns testCar
assertEquals(testCar, buildCar())
verify { buildCar() } La moquerie d'une fonction effacera toutes les simulations existantes d'autres fonctions déclarées dans le même fichier, équivalent à appeler clearStaticMockk sur la classe d'encluse générée.
Il existe trois types de fonction d'extension à Kotlin:
Pour un objet ou une classe, vous pouvez se moquer des fonctions d'extension simplement en créant un mockk ordinaire:
data class Obj ( val value : Int )
class Ext {
fun Obj. extensionFunc () = value + 5
}
with (mockk< Ext >()) {
every {
Obj ( 5 ).extensionFunc()
} returns 11
assertEquals( 11 , Obj ( 5 ).extensionFunc())
verify {
Obj ( 5 ).extensionFunc()
}
} Pour simuler les fonctions d'extension à l'échelle du module, vous devez créer mockkStatic(...) avec le nom de classe du module comme argument. Par exemple "pkg.filekt" pour module File.kt dans le package pkg .
data class Obj ( val value : Int )
// declared in File.kt ("pkg" package)
fun Obj. extensionFunc () = value + 5
mockkStatic( " pkg.FileKt " )
every {
Obj ( 5 ).extensionFunc()
} returns 11
assertEquals( 11 , Obj ( 5 ).extensionFunc())
verify {
Obj ( 5 ).extensionFunc()
} Dans les environnements jvm , vous pouvez remplacer le nom de classe par une référence de fonction:
mockkStatic( Obj ::extensionFunc) Notez que cela se moquera de toute la classe pkg.FileKt , et pas seulement extensionFunc .
Cette syntaxe s'applique également aux propriétés d'extension:
val Obj .squareValue get() = value * value
mockkStatic( Obj ::squareValue) Si @JvmName est utilisé, spécifiez-le comme nom de classe.
Khttp.kt:
@file:JvmName( " KHttp " )
package khttp
// ... KHttp code Code de test:
mockkStatic( " khttp.KHttp " ) Parfois, vous devez en savoir un peu plus pour se moquer d'une fonction d'extension. Par exemple, la fonction d'extension File.endsWith() a un classname totalement imprévisible:
mockkStatic( " kotlin.io.FilesKt__UtilsKt " )
every { File ( " abc " ).endsWith(any< String >()) } returns true
println ( File ( " abc " ).endsWith( " abc " )) Il s'agit d'un comportement standard de Kotlin qui peut être imprévisible. Utilisez Tools -> Kotlin -> Show Kotlin Bytecode ou vérifier les fichiers .class dans l'archive JAR pour détecter ces noms.
À partir de la version 1.9.1, une manipulation plus étendue de Vararg est possible:
interface ClsWithManyMany {
fun manyMany ( vararg x : Any ): Int
}
val obj = mockk< ClsWithManyMany >()
every { obj.manyMany( 5 , 6 , * varargAll { it == 7 }) } returns 3
println (obj.manyMany( 5 , 6 , 7 )) // 3
println (obj.manyMany( 5 , 6 , 7 , 7 )) // 3
println (obj.manyMany( 5 , 6 , 7 , 7 , 7 )) // 3
every { obj.manyMany( 5 , 6 , * anyVararg(), 7 ) } returns 4
println (obj.manyMany( 5 , 6 , 1 , 7 )) // 4
println (obj.manyMany( 5 , 6 , 2 , 3 , 7 )) // 4
println (obj.manyMany( 5 , 6 , 4 , 5 , 6 , 7 )) // 4
every { obj.manyMany( 5 , 6 , * varargAny { nArgs > 5 }, 7 ) } returns 5
println (obj.manyMany( 5 , 6 , 4 , 5 , 6 , 7 )) // 5
println (obj.manyMany( 5 , 6 , 4 , 5 , 6 , 7 , 7 )) // 5
every {
obj.manyMany( 5 , 6 , * varargAny {
if (position < 3 ) it == 3 else it == 4
}, 7 )
} returns 6
println (obj.manyMany( 5 , 6 , 3 , 4 , 7 )) // 6
println (obj.manyMany( 5 , 6 , 3 , 4 , 4 , 7 )) // 6Si vous avez besoin de se moquer des fonctions privées, vous pouvez le faire via un appel dynamique.
class Car {
fun drive () = accelerate()
private fun accelerate () = " going faster "
}
val mock = spyk< Car >(recordPrivateCalls = true )
every { mock[ " accelerate " ]() } returns " going not so fast "
assertEquals( " going not so fast " , mock.drive())
verifySequence {
mock.drive()
mock[ " accelerate " ]()
} Si vous souhaitez vérifier les appels privés, vous devez créer un spyk avec recordPrivateCalls = true
De plus, une syntaxe plus verbeuse vous permet d'obtenir et de définir des propriétés, combinées avec les mêmes appels dynamiques:
val mock = spyk( Team (), recordPrivateCalls = true )
every { mock getProperty " speed " } returns 33
every { mock setProperty " acceleration " value less( 5 ) } just runs
justRun { mock invokeNoArgs " privateMethod " }
every { mock invoke " openDoor " withArguments listOf ( " left " , " rear " ) } returns " OK "
verify { mock getProperty " speed " }
verify { mock setProperty " acceleration " value less( 5 ) }
verify { mock invoke " openDoor " withArguments listOf ( " left " , " rear " ) } Vous pouvez accéder aux champs de support via fieldValue et utiliser value pour la valeur définie.
Remarque: Dans les exemples ci-dessous, nous utilisons propertyType pour spécifier le type de fieldValue . Cela est nécessaire car il est possible de capturer automatiquement le type pour le Getter. Utilisez nullablePropertyType pour spécifier un type nullable.
val mock = spyk( MockCls (), recordPrivateCalls = true )
every { mock.property } answers { fieldValue + 6 }
every { mock.property = any() } propertyType Int :: class answers { fieldValue + = value }
every { mock getProperty " property " } propertyType Int :: class answers { fieldValue + 6 }
every { mock setProperty " property " value any< Int >() } propertyType Int :: class answers { fieldValue + = value }
every {
mock.property = any()
} propertyType Int :: class answers {
fieldValue = value + 1
} andThen {
fieldValue = value - 1
}Ajout de comportements supplémentaires via les interfaces et les faire tomber:
val spy = spyk( System . out , moreInterfaces = arrayOf( Runnable :: class ))
spy. println ( 555 )
every {
(spy as Runnable ). run ()
} answers {
(self as PrintStream ). println ( " Run! Run! Run! " )
}
val thread = Thread (spy as Runnable )
thread.start()
thread.join() Rien de spécial ici. Si vous avez une fonction qui ne renvoie Nothing :
fun quit ( status : Int ): Nothing {
exitProcess(status)
}Ensuite, vous pouvez par exemple lancer une exception en tant que comportement:
every { quit( 1 ) } throws Exception ( " this is a test " ) Une simulation de lunette est une simulation qui se déclenche automatiquement après que le bloc de code passé lors d'un paramètre a été exécuté. Vous pouvez utiliser les fonctions mockkObject , mockkStatic et mockkConstructor .
object ObjBeingMocked {
fun add ( a : Int , b : Int ) = a + b
}
// ObjBeingMocked will be unmocked after this scope
mockkObject( ObjBeingMocked ) {
assertEquals( 3 , ObjBeingMocked .add( 1 , 2 ))
every { ObjBeingMocked .add( 1 , 2 ) } returns 55
assertEquals( 55 , ObjBeingMocked .add( 1 , 2 ))
} Un moyen très simple de créer de nouveaux correspondants consiste à attacher une fonction à MockKMatcherScope ou MockKVerificationScope et en utilisant la fonction match :
fun MockKMatcherScope. seqEq ( seq : Sequence < String >) = match< Sequence < String >> {
it.toList() == seq.toList()
} Il est également possible de créer des correspondances plus avancées en implémentant l'interface Matcher .
Exemple de correspondant personnalisé qui compare la liste sans commande:
@Test
fun test () {
class MockCls {
fun op ( a : List < Int >) = a.reversed()
}
val mock = mockk< MockCls >()
every { mock.op(any()) } returns listOf ( 5 , 6 , 9 )
println (mock.op( listOf ( 1 , 2 , 3 )))
verify { mock.op(matchListWithoutOrder( 3 , 2 , 1 )) }
}
data class ListWithoutOrderMatcher < T >(
val expectedList : List < T >,
val refEq : Boolean
) : Matcher<List<T>> {
val map = buildCountsMap(expectedList, refEq)
override fun match ( arg : List < T > ? ): Boolean {
if (arg == null ) return false
return buildCountsMap(arg, refEq) == map
}
private fun buildCountsMap ( list : List < T >, ref : Boolean ): Map < Any ?, Int > {
val map = mutableMapOf< Any ?, Int >()
for (item in list) {
val key = when {
item == null -> nullKey
refEq -> InternalPlatform .ref(item)
else -> item
}
map.compute(key, { _, value -> (value ? : 0 ) + 1 })
}
return map
}
override fun toString () = " matchListWithoutOrder( $expectedList ) "
@Suppress( " UNCHECKED_CAST " )
override fun substitute ( map : Map < Any , Any >): Matcher < List < T >> {
return copy(expectedList = expectedList.map { map.getOrDefault(it as Any? , it) } as List < T >)
}
companion object {
val nullKey = Any ()
}
}
inline fun < reified T : List < E >, E : Any > MockKMatcherScope. matchListWithoutOrder (
vararg items : E ,
refEq : Boolean = true
): T = match( ListWithoutOrderMatcher ( listOf ( * items), refEq))Exemple utilisant la réflexion pour se moquer de toutes les méthodes sur un objet de style constructeur
val builderFunctions = MyBuilder :: class .memberFunctions.filter { it.returnType.classifier == MyBuilder :: class }
val builderMock = mockk< MyBuilder > {
builderFunctions.forEach { func ->
every {
val params = listOf< Any ?>(builderMock) + func.parameters.drop( 1 ).map { any(it.type.classifier as KClass < Any >) }
func.call( * params.toTypedArray())
} answers {
this @mockk
}
}
}Pour ajuster les paramètres à l'échelle mondiale, vous pouvez spécifier quelques paramètres dans un fichier de ressources.
Comment utiliser:
io/mockk/settings.properties dans src/main/resources . relaxed =true|false
relaxUnitFun =true|false
recordPrivateCalls =true|false
stackTracesOnVerify =true|false
stackTracesAlignment =left|center stackTracesAlignment détermine s'il faut aligner les traces de pile sur le centre (par défaut) ou vers la gauche (plus cohérente avec des stacktraces JVM habituelles).
Voici quelques tables pour vous aider à maîtriser le DSL.
| Fonction | Description |
|---|---|
mockk<T>(...) | construit une simulation régulière |
spyk<T>() | construit un espion en utilisant le constructeur par défaut |
spyk(obj) | construit un espion en copie à partir d' obj |
slot | Crée une fente de capture |
every | commence un bloc de coups |
coEvery | commence un bloc de coups pour les coroutines |
verify | démarre un bloc de vérification |
coVerify | démarre un bloc de vérification pour les coroutines |
verifyAll | démarre un bloc de vérification qui devrait inclure tous les appels |
coVerifyAll | démarre un bloc de vérification qui devrait inclure tous les appels pour les coroutines |
verifyOrder | démarre un bloc de vérification qui vérifie la commande |
coVerifyOrder | démarre un bloc de vérification qui vérifie la commande des coroutines |
verifySequence | démarre un bloc de vérification qui vérifie si tous les appels ont été effectués dans une séquence spécifiée |
coVerifySequence | démarre un bloc de vérification qui vérifie si tous les appels ont été effectués dans une séquence spécifiée pour les coroutines |
excludeRecords | exclure certains appels de l'enregistrement |
confirmVerified | confirme que tous les appels enregistrés ont été vérifiés |
checkUnnecessaryStub | confirme que tous les appels enregistrés sont utilisés au moins une fois |
clearMocks | efface les simulations spécifiées |
registerInstanceFactory | vous permet de redéfinir la voie d'instanciation pour certains objets |
mockkClass | construit une maquette régulière en faisant passer la classe en tant que paramètre |
mockkObject | transforme un objet en une simulation d'objet, ou l'efface si elle était déjà transformée |
unmockkObject | transforme une maquette d'objet en un objet ordinaire |
mockkStatic | Fait une simulation statique d'une classe, ou l'efface si elle était déjà transformée |
unmockkStatic | transforme une simulation statique en classe régulière |
clearStaticMockk | efface une simulation statique |
mockkConstructor | Fait une simulation de constructeur d'une classe, ou l'efface s'il était déjà transformé |
unmockkConstructor | transforme une simulation de constructeur en une classe régulière |
clearConstructorMockk | Efface la simulation du constructeur |
unmockkAll | UNMOCKS OBJET, STATIQUE ET MOCHES |
clearAllMocks | Efface les simulations régulières, objets, statiques et constructeurs |
Par défaut, les arguments simples sont appariés à l'aide eq()
| Correspondant | Description |
|---|---|
any() | correspond à n'importe quel argument |
any(Class) | correspond à tout argument de la classe Give (pour une moquerie réfléchissante) |
allAny() | Matcher spécial qui utilise any() au lieu d' eq() pour les correspondances qui sont fournies comme des arguments simples |
isNull() | vérifie si la valeur est nul |
isNull(inverse=true) | vérifie si la valeur n'est pas nul |
ofType(type) | vérifie si la valeur appartient au type |
match { it.startsWith("string") } | correspond via le prédicat passé |
coMatch { it.startsWith("string") } | correspond via le prédicat coroutine passé |
matchNullable { it?.startsWith("string") } | correspond à une valeur nullable via le prédicat passé |
coMatchNullable { it?.startsWith("string") } | correspond à une valeur nullable via le prédicat coroutine passé |
eq(value) | correspond si la valeur est égale à la valeur fournie via la fonction deepEquals |
eq(value, inverse=true) | correspond si la valeur n'est pas égale à la valeur fournie via la fonction deepEquals |
neq(value) | correspond si la valeur n'est pas égale à la valeur fournie via la fonction deepEquals |
refEq(value) | correspond si la valeur est égale à la valeur fournie via la comparaison de référence |
refEq(value, inverse=true) | correspond si la valeur n'est pas égale à la valeur fournie via la comparaison de référence |
nrefEq(value) | correspond si la valeur n'est pas égale à la valeur fournie via la comparaison de référence |
cmpEq(value) | correspond si la valeur est égale à la valeur fournie via la fonction compareTo |
less(value) | correspond si la valeur est inférieure à la valeur fournie via la fonction compareTo |
more(value) | correspond si la valeur est supérieure à la valeur fournie via la fonction compareTo |
less(value, andEquals=true) | correspond si la valeur est inférieure ou égale à la valeur fournie via la fonction compareTo |
more(value, andEquals=true) | correspond si la valeur est supérieure ou égale à la valeur fournie via la fonction compareTo |
range(from, to, fromInclusive=true, toInclusive=true) | correspond si la valeur est dans la plage via la fonction compareTo |
and(left, right) | combine deux correspondants via un logique et |
or(left, right) | combine deux matchs via un logique ou |
not(matcher) | nie le correspondant |
capture(slot) | capture une valeur non nullable à un CapturingSlot |
captureNullable(slot) | capture une valeur nullable à un CapturingSlot |
capture(mutableList) | capture une valeur à une liste |
captureNullable(mutableList) | capture une valeur à une liste avec des valeurs nulles |
captureLambda() | capture un lambda |
captureCoroutine() | capture une coroutine |
invoke(...) | appelle un argument apparié |
coInvoke(...) | appelle un argument apparié pour une coroutine |
hint(cls) | laisse entendre le type de retour suivant au cas où il se serait effacé |
anyVararg() | correspond à tous les éléments dans un vararg |
varargAny(matcher) | correspond si un élément correspond au match |
varargAll(matcher) | matchs si tous les éléments correspondent au correspondant |
any...Vararg() | correspond à tous les éléments de Vararg (spécifiques au type primitif) |
varargAny...(matcher) | correspond à un élément correspond au matchur (spécifique au type primitif) |
varargAll...(matcher) | correspond aux correspondances si tous les éléments correspondent au correspondant (spécifique au type primitif) |
Quelques matchs spéciaux disponibles en mode vérification uniquement:
| Correspondant | Description |
|---|---|
withArg { code } | correspond à n'importe quelle valeur et permet d'exécuter un code |
withNullableArg { code } | correspond à toute valeur nullable et permet d'exécuter un code |
coWithArg { code } | correspond à n'importe quelle valeur et permet d'exécuter un code coroutine |
coWithNullableArg { code } | correspond à toute valeur nullable et permet d'exécuter un code coroutine |
| Validateur | Description |
|---|---|
verify { mock.call() } | Faire une vérification non ordonnée qu'un appel a été effectué |
verify(inverse=true) { mock.call() } | Faire une vérification non ordonnée qu'un appel n'a pas été effectué |
verify(atLeast=n) { mock.call() } | Faire une vérification non ordonnée qu'un appel a été effectué au moins n fois |
verify(atMost=n) { mock.call() } | Faire une vérification non ordonnée qu'un appel a été effectué au plus n fois |
verify(exactly=n) { mock.call() } | Faire une vérification non ordonnée qu'un appel a été effectué exactement n fois |
verifyAll { mock.call1(); mock.call2() } | Faire une vérification non ordonnée que seuls les appels spécifiés ont été exécutés pour les simulations mentionnées |
verifyOrder { mock.call1(); mock.call2() } | Vérifiez que la séquence des appels est passée les unes après les autres |
verifySequence { mock.call1(); mock.call2() } | Faire la vérification que seule la séquence d'appels spécifiée a été exécutée pour les simulations mentionnées |
verify { mock wasNot Called } | Vérification qu'un simulation n'a pas été appelé |
verify { listOf(mock1, mock2) wasNot Called } | Vérification qu'une liste de simulations n'a pas été appelée |
Une réponse peut être suivie d'une ou plusieurs réponses supplémentaires.
| Répondre | Description |
|---|---|
returns value | Spécifiez que l'appel correspondant renvoie une valeur spécifiée |
returnsMany list | Spécifiez que l'appel correspondant renvoie une valeur de la liste, avec des appels ultérieurs renvoyant l'élément suivant |
returnsArgument(n) | Spécifiez que l'appel apparié renvoie l'argument nème de cet appel |
throws ex | Spécifiez que l'appel assorti lance une exception |
throwsMany list | Spécifiez que l'appel correspondant jette une exception de la liste, avec des appels ultérieurs lançant l'exception suivante |
answers { code } | Spécifiez que l'appel correspondant répond à un bloc de code parcouru avec answer scope |
coAnswers { code } | Spécifiez que l'appel correspondant répond à un bloc de code Coroutine avec answer scope |
answers answerObj | Spécifiez que l'appel apparié répond avec un objet de réponse |
answers { nothing } | Spécifiez que l'appel apparié répond null |
just Runs | Spécifiez que l'appel correspondant est un unité de retour (renvoie NULL) |
just Awaits | Spécifiez que l'appel assorti ne revient jamais (disponible depuis V1.13.3) |
propertyType Class | Spécifiez le type d'accessoire de champ de support |
nullablePropertyType Class | Spécifiez le type d'accessoire de champ de support comme type nullable |
Une réponse suivante est retournée sur chaque appel consécutif et la dernière valeur est persistée. C'est donc similaire à la sémantique returnsMany .
| Réponse supplémentaire | Description |
|---|---|
andThen value | Spécifiez que l'appel correspondant renvoie une valeur spécifiée |
andThenMany list | Spécifiez que l'appel correspondant renvoie une valeur de la liste, avec des appels ultérieurs renvoyant l'élément suivant |
andThenThrows ex | Spécifiez que l'appel assorti lance une exception |
andThenThrowsMany ex | Spécifiez que l'appel correspondant jette une exception de la liste, avec des appels ultérieurs lançant l'exception suivante |
andThen { code } | Spécifiez que l'appel correspondant répond à un bloc de code parcouru avec answer scope |
coAndThen { code } | Spécifiez que l'appel correspondant répond à un bloc de code Coroutine avec answer scope |
andThenAnswer answerObj | Spécifiez que l'appel apparié répond avec un objet de réponse |
andThen { nothing } | Spécifiez que l'appel apparié répond null |
andThenJust Runs | Spécifiez que l'appel correspondant est une unité de retour (disponible depuis V1.12.2) |
andThenJust Awaits | Spécifiez que l'appel correspondant n'est jamais retourné (disponible depuis V1.13.3) |
| Paramètre | Description |
|---|---|
call | un objet d'appel qui se compose d'une invocation et d'un match |
invocation | Contient des informations sur la fonction réelle invoquée |
matcher | Contient des informations concernant le correspondant utilisé pour correspondre à l'invocation |
self | Référence à l'invocation de l'objet faite |
method | Référence à l'invocation de la fonction faite |
args | Référence aux arguments d'invocation |
nArgs | Nombre d'arguments d'invocation |
arg(n) | Nième argument |
firstArg() | premier argument |
secondArg() | deuxième argument |
thirdArg() | troisième argument |
lastArg() | dernier argument |
captured() | le dernier élément de la liste des commodités lors de la capture d'une liste |
lambda<...>().invoke() | Appelez le lambda capturé |
coroutine<...>().coInvoke() | Appelez la coroutine capturée |
nothing | valeur nulle pour nothing renvoyer comme réponse |
fieldValue | Accessoire du champ de soutien à la propriété |
fieldValueAny | Accessoire du champ de support de propriété avec Any? taper |
value | valeur définie, coulée sur le même type que le champ de sauvegarde de la propriété |
valueAny | la valeur étant définie, avec Any? taper |
callOriginal | appelle la fonction d'origine |
| Paramètre | Description |
|---|---|
position | la position d'un argument dans un tableau vararg |
nArgs | Compte global des arguments dans un tableau Vararg |
Vous pouvez également soutenir ce projet en devenant un sponsor. Votre logo apparaîtra ici avec un lien vers votre site Web.
Merci à tous nos bailleurs de fonds!
Ce projet existe grâce à toutes les personnes qui contribuent.
Pour poser des questions, veuillez utiliser Stack Overflow ou Gitter.
Pour signaler les bogues, veuillez utiliser le projet GitHub.