How do I integrate ScalaTest with Spring

2020-02-19 09:29发布

问题:

I need to populate my ScalaTest tests with @Autowired fields from a Spring context, but most Scalatest tests (eg FeatureSpecs can't be run by the SpringJUnit4ClassRunner.class -

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="myPackage.UnitTestSpringConfiguration", loader=AnnotationConfigContextLoader.class)
public class AdminLoginTest {
    @Autowired private WebApplication app;
    @Autowired private SiteDAO siteDAO;

(Java, but you get the gist).

How do I populate @Autowired fields from an ApplicationContext for ScalaTest?

class AdminLoginFeatureTest extends FeatureSpec with GivenWhenThen with ShouldMatchersForJUnit {

  @Autowired val app: WebApplication = null
  @Autowired val siteDAO: SiteDAO = null

  feature("Admin Login") {
    scenario("Correct username and password") {...}

回答1:

Use the TestContextManager, as this caches the contexts so that they aren't rebuilt every test. It is configured from the class annotations.

@ContextConfiguration(
  locations = Array("myPackage.UnitTestSpringConfiguration"), 
  loader = classOf[AnnotationConfigContextLoader])
class AdminLoginFeatureTest extends FeatureSpec with GivenWhenThen with ShouldMatchers {

  @Autowired val app: WebApplication = null
  @Autowired val siteDAO: SiteDAO = null
  new TestContextManager(this.getClass()).prepareTestInstance(this)

  feature("Admin Login") {
    scenario("Correct username and password") {...}
  }
}


回答2:

I tried using Duncan's answer with Spring 4 + Scala 2.11 and I got the following error:

java.lang.IllegalStateException: Test class [TestGateway] has been configured with @ContextConfiguration's 'locations' (or 'value') attribute {GatewayContextConfiguration}, but AnnotationConfigContextLoader does not support resource locations.

I had to tweak his code to use a class when configuring the ContextConfiguration instead of a string:

@ContextConfiguration( classes = Array(classOf[GatewayContextConfiguration]), 
                       loader = classOf[AnnotationConfigContextLoader])
class TestGateway extends FlatSpec with Matchers {

  @Autowired val gpClient: IGlobalPropsWSClient = null

  new TestContextManager(this.getClass()).prepareTestInstance(this)

  "Echo" should "return what it was sent." in {
    val gateway = new CasaWsGateway
    gateway.echo("This is a test") should be ( "This is a test" )
  }
}


回答3:

Here's a version implemented as a stackable trait (so that you can have your own beforeAll() and afterAll() methods, if necessary) that uses TestContextManager to complete the context lifecycle.

I tried the raw TestContextManager.prepareTestInstance() solution suggested in other posts, but noticed that my contexts weren't getting closed which results in side effects and accumulating garbage after each successive test run when using the sbt console.

@ContextConfiguration(classes = Array(classOf[SomeConfiguration]))
class SomeTestSpec extends FlatSpec with TestContextManagement {

  // Use standard Autowired Spring annotation to inject necessary dependencies
  // Note that Spring will inject val (read-only) fields
  @Autowired
  val someDependency: SomeClass = null

  "Some test" should "verify something" in {
    // Test implementation that uses injected dependency
  }

}

TestContextManagement Gist

import org.scalatest.{BeforeAndAfterAll, Suite}
import org.springframework.core.annotation.{AnnotatedElementUtils, AnnotationAttributes}
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.{TestContext, TestContextManager}
import org.springframework.test.context.support.DirtiesContextTestExecutionListener
import org.springframework.util.Assert

/**
 * Manages Spring test contexts via a TestContextManager.
 *
 * Implemented as a stackable trait that uses beforeAll() and afterAll() hooks to invoke initialization
 * and destruction logic, respectively.
 * Test contexts are marked dirty, and hence cleaned up, after all test methods have executed.
 * There is currently no support for indicating that a test method dirties a context.
 *
 * @see org.springframework.test.context.TestContextManager
 */
trait TestContextManagement extends BeforeAndAfterAll { this: Suite =>

  private val testContextManager: TestContextManager = new TestContextManager(this.getClass)

  abstract override def beforeAll(): Unit = {
    super.beforeAll
    testContextManager.registerTestExecutionListeners(AlwaysDirtiesContextTestExecutionListener)
    testContextManager.beforeTestClass
    testContextManager.prepareTestInstance(this)
  }

  abstract override def afterAll(): Unit = {
    testContextManager.afterTestClass
    super.afterAll
  }
}

/**
 * Test execution listener that always dirties the context to ensure that contexts get cleaned after test execution.
 *
 * Note that this class dirties the context after all test methods have run.
 */
protected object AlwaysDirtiesContextTestExecutionListener extends DirtiesContextTestExecutionListener {

  @throws(classOf[Exception])
  override def afterTestClass(testContext: TestContext) {
    val testClass: Class[_] = testContext.getTestClass
    Assert.notNull(testClass, "The test class of the supplied TestContext must not be null")

    val annotationType: String = classOf[DirtiesContext].getName
    val annAttrs: AnnotationAttributes = AnnotatedElementUtils.getAnnotationAttributes(testClass, annotationType)
    val hierarchyMode: DirtiesContext.HierarchyMode = if ((annAttrs == null)) null else annAttrs.getEnum[DirtiesContext.HierarchyMode]("hierarchyMode")
    dirtyContext(testContext, hierarchyMode)
  }
}


回答4:

If you are using Spring boot you could use the TestContextManager (as other comments suggested) and the @SpringBootTest annotation.

This is how I test a controller with scalaTest and spring boot:

@RunWith(classOf[SpringRunner])
@SpringBootTest(webEnvironment = RANDOM_PORT)
class CustomerControllerIT extends FeatureSpec with GivenWhenThen with Matchers {

  @Autowired
  var testRestTemplate: TestRestTemplate = _
  new TestContextManager(this.getClass).prepareTestInstance(this)

  @LocalServerPort
  val randomServerPort: Integer = null

  val baseUrl = s"http://localhost:$randomServerPort"

  feature("Customer controller") {

    scenario("Find customer by id") {

      Given("a customer id")
      val id = 1

      When("a request to /customers/{id} is sent")
      val url = s"$baseUrl/customers/$id"
      val response = testRestTemplate.getForEntity(url, classOf[Customer])

      Then("we get a response with the customer in the body")
      response.getBody.getId shouldBe 1
      response.getBody.getName shouldBe "Bob"

    }

  }

}

Here there is a post about how to do integration tests and unit tests with spring boot and ScalaTest: ignaciosuay.com/testing-spring-boot-with-scalatest/