# Integration Tests: API Interaction

> For the complete documentation index, see [llms.txt](https://plugins.jetbrains.com/docs/intellij/llms.txt).

> **Tip:**
> This page is part of the [Integration Tests](integration-tests.html) tutorial.

For introduction and setting up dependencies, refer to [Introduction to Integration Tests](integration-tests-intro.html).

## Java Management Extension

The IDE and tests are running in different processes, which means some way to communicate between them is required.

Driver framework uses [Java Management Extensions](https://en.wikipedia.org/wiki/Java_Management_Extensions) (or JMX for short). JMX supports different connectors to manage the state of the JVM. Driver framework uses a standard Java Remote Method Invocation (RMI) protocol. This protocol allows tests to access objects and invoke methods in the JVM running the IDE.

The architecture of RMI protocol is as follows:

```PLANTUML
@startuml
box "Test Process" #F0F0F0
    participant "Test (Client)" as Test
end box

box "IDE Process" #F0F0F0
    participant "IDE (Server)" as IDE
    participant "RMI registry" as Registry
end box

Test ->> Registry: 1. Look up remote object
Registry -->> Test: 2. Return stub reference

note over Test
    Stub is obtained
    for remote reference
end note

activate Test
Test ->> IDE: 3. Remote method call via stub
IDE ->> IDE: 4. Process request and invoke actual method
IDE -->> Test: 5. Return serialized result
deactivate Test

note over Test,IDE
    Each remote call involves:
    - Serialization of parameters
    - Network transfer
    - Deserialization
    - Method invocation
    - Serialization of result
end note
@enduml
```

When a test needs to invoke a method on a remote object:

1. The test looks up the remote object in the RMI registry (the IDE).

2. The IDE returns a reference to the stub, which implements the same interface as the remote object.

3. When the client calls a method on the stub:

1. The stub serializes the method call, including any parameters.

2. It sends the serialized data over the network to the remote server.

3. The RMI runtime inside the IDE unmarshals the request and invokes the corresponding method on the actual remote object.

4. The method executes, and the result is serialized and sent back to the client via the stub.

5. The stub unmarshals the response and returns the result to the caller.

Thus, the stub makes remote calls feel like local method calls, without the caller worrying about low-level networking, data conversion, or request handling.

## Creating Stubs

To demonstrate how this works in practice, add the following code to the plugin:

```KOTLIN
package com.example.demo

import com.intellij.openapi.components.Service

object PluginStorage {
  @JvmStatic
  fun getPluginStorage() = Storage(
    "static method", listOf("static1", "static2")
  )
}

@Service
class PluginService {
  fun someMethod(): Unit = Unit
  fun getAnswer(): Int = 42
}

@Service(Service.Level.PROJECT)
class PluginProjectService {
  fun getStrings(): Array<String> = arrayOf("foo", "bar")
}

data class Storage(val key: String, val attributes: List<String>)
```

This file contains one class with the static method `getPluginStorage`, and two [light services](plugin-services.html#light-services) – one application-level `PluginService` and one project-level `PluginProjectService`.

The test needs to call the methods of these classes and verify their return values. To do this, the following stubs have to be created:

```KOTLIN
import com.intellij.driver.client.Remote

@Remote("com.example.demo.PluginStorage", plugin = "com.example.demo")
interface PluginStorage {
  fun getPluginStorage(): Storage
}

@Remote("com.example.demo.PluginService", plugin = "com.example.demo")
interface PluginService {
  fun getAnswer(): Int
}

@Remote("com.example.demo.PluginProjectService", plugin = "com.example.demo")
interface PluginProjectService {
  fun getStrings(): Array<String>
}

@Remote("com.example.demo.Storage", plugin = "com.example.demo")
interface Storage {
  fun getAttributes(): List<String>
  fun getKey(): String
}
```

As seen above, a stub is an interface for the class with methods that will be used in the test. Stubs shouldn't be created for methods not used in tests.

The first parameter is the fully qualified name of the class that will correspond to the stub using the [Remote](https://github.com/JetBrains/intellij-community/tree/idea/263.7307.200/platform/remote-driver/client/src/com/intellij/driver/client/Remote.kt) annotation. Strings are used to avoid introducing dependency between production and test code.

The `plugin` parameter specifies the ID of a plugin, where classes are located. This parameter is required since IntelliJ-based IDEs use separate [class loaders](plugin-class-loaders.html) for each plugin, and the code that will call methods on the IDE side (Invoker) needs to know where to search for them.

There is built-in support for `@Remote` annotation inside IntelliJ IDEA:

![Remote support](images/remote-support.png)

Rename and move the target class refactorings will update the annotation accordingly. Gutter icons can be used to navigate to the target class from a stub.

## Calling IDE Methods from Tests

The complete API test:

```KOTLIN
@Test
fun testStubs() {
  Starter.newContext(
    "testExample",
    TestCase(
      IdeInfo.IdeaUltimate,
      GitHubProject.fromGithub(
        branchName = "master",
        repoRelativeUrl = "JetBrains/ij-perf-report-aggregator"
      )
    )
  )
    .apply {
      val pathToPlugin = System.getProperty("path.to.build.plugin")
      PluginConfigurator(this).installPluginFromFolder(File(pathToPlugin))
    }.runIdeWithDriver().useDriverAndCloseIde {
      val storage = utility<PluginStorage>().getPluginStorage()
      val key = storage.getKey()
      val attributes = storage.getAttributes()
      Assertions.assertEquals("static method", key)
      Assertions.assertEquals(listOf("static1", "static2"), attributes)
      val answer = service<PluginService>().getAnswer()
      Assertions.assertEquals(42, answer)
      waitForProjectOpen()
      val project = singleProject()
      val strings = service<PluginProjectService>(project).getStrings()
      Assertions.assertArrayEquals(arrayOf("foo", "bar"), strings)
    }
}
```

There are two [Driver](https://github.com/JetBrains/intellij-community/tree/idea/263.7307.200/platform/remote-driver/client/src/com/intellij/driver/client/Driver.kt) extension methods that allow invoking methods: `service()` and `utility()`. The first one will return an instance of a service, and the second will return an instance of any class.

Project-level services require a `Project` stub. To get it, use the `singleProject` method, which is implemented in the same way as demonstrated above:

```KOTLIN
service<ProjectManager>().getOpenProjects().singleOrNull()
```

> **Note:**
> Service and utility proxies can be acquired on each call, there is no need to cache them in clients.

## Contexts and Remote References

To prevent memory leaks, Driver uses `java.lang.ref.WeakReference` for call results.

Consider the following example:

```KOTLIN
val roots = driver.service(ProjectRootManager::class, driver.singleProject()).getContentRoots()
val name = roots[0].getName() // may throw an error
```

In many cases, it throws an exception:

```TEXT
Weak reference to variable x expired. Please use `Driver.withContext { }` for hard variable references.
```

To keep results alive across calls, wrap them in a context:

```KOTLIN
driver.withContext {
  val roots = service<ProjectRootManager>(singleProject()).getContentRoots()
  val name = roots[0].getName()

  // results computed inside are guaranteed to be alive until the end of the block
}
```

Contexts can be nested, and they can be used independently in helper methods, e.g.:

```KOTLIN
fun Driver.importGradleProject(project: Project? = null) {
  withContext {
    val forProject = project ?: singleProject()
    utility(ImportGradleProjectUtil::class).importProject(forProject)
  }
}
```

## JMX/RMI Limitations

The main inconvenience of using JMX/RMI is that stubs need to be created for all objects which are used in tests. On the other hand, they don't require modification of the production code.

As with any protocol, JMX/RMI has its limitations:

* Parameters and return values can only be: * Primitives and their wrappers: `Integer`, `Short`, `Long`, `Double`, `Float`, and `Byte`. * `String`. * `@Remote` references. * An array of primitive values or `String` or `@Remote` references. * Lists of primitive values or `String` or `@Remote` references.

* Only public methods can be called.

* JMX/RMI can’t interact with suspend methods.

