Skip to content

Commit

Permalink
Force using correct mixin of MockFactory in async/sync tests (#451)
Browse files Browse the repository at this point in the history
* feat: Use self-type instead of inheritance

It is helpful to prevent mistakenly mixing synchronous tests with asynchronous mock-factory and vice versa

* test: Test invalid sync-async combination are prevented at compile-time

Co-authored-by: nima.taheri@hootsuite.com <nima.taheri@hootsuite.com>
  • Loading branch information
nimatrueway and nima-taheri-hs committed Jul 25, 2022
1 parent 0df3354 commit e8ab8f2
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 34 deletions.
Expand Up @@ -6,7 +6,7 @@ import org.scalatest.exceptions.{StackDepthException, TestFailedException}
import scala.concurrent.Future
import scala.util.control.NonFatal

trait AbstractAsyncMockFactory extends AsyncTestSuiteMixin with AsyncMockFactoryBase with AsyncTestSuite {
trait AbstractAsyncMockFactory extends AsyncTestSuiteMixin with AsyncMockFactoryBase { this: AsyncTestSuite =>

type ExpectationException = TestFailedException

Expand Down
Expand Up @@ -24,21 +24,21 @@ import org.scalamock.MockFactoryBase
import org.scalatest.exceptions.{StackDepthException, TestFailedException}
import org.scalatest._

trait AbstractMockFactory extends TestSuiteMixin with MockFactoryBase with TestSuite {
trait AbstractMockFactory extends TestSuiteMixin with MockFactoryBase { this: TestSuite =>

type ExpectationException = TestFailedException

abstract override def withFixture(test: NoArgTest): Outcome = {

if (autoVerify) {
withExpectations {
withExpectations {
val outcome = super.withFixture(test)
outcome match {
case Failed(throwable) =>
case Failed(throwable) =>
// MockFactoryBase does not know how to handle ScalaTest Outcome.
// Throw error that caused test failure to prevent hiding it by
// Throw error that caused test failure to prevent hiding it by
// "unsatisfied expectation" exception (see issue #72)
throw throwable
throw throwable
case _ => outcome
}
}
Expand Down
Expand Up @@ -3,4 +3,6 @@ package org.scalamock.scalatest
import org.scalamock.clazz.Mock
import org.scalatest.AsyncTestSuite

trait AsyncMockFactory extends AbstractAsyncMockFactory with Mock with AsyncTestSuite
trait AsyncMockFactory extends AbstractAsyncMockFactory with Mock { this: AsyncTestSuite =>

}
Expand Up @@ -2,6 +2,7 @@ package org.scalamock.scalatest

import org.scalamock.clazz.{Mock => MacroMock}
import org.scalamock.proxy.ProxyMockFactory
import org.scalatest.TestSuite

import scala.reflect.ClassTag

Expand All @@ -12,7 +13,7 @@ import scala.reflect.ClassTag
* val proxyMock = Proxy.mock[Bar]
* }}}
*/
trait MixedMockFactory extends AbstractMockFactory with MacroMock {
trait MixedMockFactory extends AbstractMockFactory with MacroMock { this: TestSuite =>

object Proxy extends ProxyMockFactory {
import org.scalamock.proxy._
Expand Down
46 changes: 24 additions & 22 deletions shared/src/main/scala/org/scalamock/scalatest/MockFactory.scala
Expand Up @@ -46,46 +46,46 @@ import org.scalatest.TestSuite
* ==Sharing mocks across test cases==
*
* Sometimes multiple test cases need to work with the same mocks (and more generally - the same
* fixtures: files, sockets, database connections, etc.). There are many techniques to avoid duplicating
* the fixture code across test cases in ScalaTest, but ScalaMock recommends and officially supports
* fixtures: files, sockets, database connections, etc.). There are many techniques to avoid duplicating
* the fixture code across test cases in ScalaTest, but ScalaMock recommends and officially supports
* these two:
* - '''isolated tests cases''' - clean and simple, recommended when all test cases have the same
* - '''isolated tests cases''' - clean and simple, recommended when all test cases have the same
* or very similar fixtures
* - '''fixture contexts''' - more flexible, recommened for complex test suites where single set of
* fixtures does not fit all test cases
*
* ===Isolated test cases===
*
* If you mix `OneInstancePerTest` trait into a `Suite`, each test case will run in its own instance
* of the suite class and therefore each test will get a fresh copy of the instance variables.
*
*
* If you mix `OneInstancePerTest` trait into a `Suite`, each test case will run in its own instance
* of the suite class and therefore each test will get a fresh copy of the instance variables.
*
* This way in the suite scope you can declare instance variables (e.g. mocks) that will be used by
* multiple test cases and perform common test case setup (e.g. set up some mock expectations).
* Because each test case has fresh instance variables different test cases do not interfere with each
* other.
*
*
* {{{
* // Please note that this test suite mixes in OneInstancePerTest
* class CoffeeMachineTest extends FlatSpec with ShouldMatchers with OneInstancePerTest with MockFactory {
* // shared objects
* val waterContainerMock = mock[WaterContainer]
* val heaterMock = mock[Heater]
* val coffeeMachine = new CoffeeMachine(waterContainerMock, heaterMock)
*
*
* // you can set common expectations in suite scope
* (waterContainerMock.isOverfull _).expects().returning(true)
*
*
* // test setup
* coffeeMachine.powerOn()
*
*
* "CoffeeMachine" should "not turn on the heater when the water container is empty" in {
* coffeeMachine.isOn shouldBe true
* // ...
* coffeeMachine.powerOff()
* }
*
*
* it should "not turn on the heater when the water container is overfull" in {
* // each test case uses separate, fresh Suite so the coffee machine is turned on
* // each test case uses separate, fresh Suite so the coffee machine is turned on
* coffeeMachine.isOn shouldBe true
* // ...
* }
Expand All @@ -94,47 +94,49 @@ import org.scalatest.TestSuite
*
* ===Fixture contexts===
*
* You can also run each test case in separate fixture context. Fixture contexts can be extended
* You can also run each test case in separate fixture context. Fixture contexts can be extended
* and combined and since each test case uses different instance of fixture context test cases do not
* interfere with each other while they can have shared mocks and expectations.
*
*
* {{{
* class CoffeeMachineTest extends FlatSpec with ShouldMatchers with MockFactory {
* trait Test { // fixture context
* // shared objects
* val waterContainerMock = mock[WaterContainer]
* val heaterMock = mock[Heater]
* val coffeeMachine = new CoffeeMachine(waterContainerMock, heaterMock)
*
*
* // test setup
* coffeeMachine.powerOn()
* }
*
*
* "CoffeeMachine" should "not turn on the heater when the water container is empty" in new Test {
* coffeeMachine.isOn shouldBe true
* (waterContainerMock.isOverfull _).expects().returning(true)
* // ...
* }
*
*
* // you can extend and combine fixture-contexts
* trait OverfullWaterContainerTest extends Test {
* // you can set expectations and use mocks in fixture-context
* (waterContainerMock.isEmpty _).expects().returning(true)
*
*
* // and define helper functions
* def complexLogic() {
* coffeeMachine.powerOff()
* // ...
* }
* }
*
*
* it should "not turn on the heater when the water container is overfull" in new OverfullWaterContainerTest {
* // ...
* complexLogic()
* }
* }
* }}}
*
*
* See [[org.scalamock]] for overview documentation.
*/
trait MockFactory extends AbstractMockFactory with Mock with TestSuite
trait MockFactory extends AbstractMockFactory with Mock { this: TestSuite =>

}
Expand Up @@ -4,4 +4,6 @@ import org.scalamock.proxy.ProxyMockFactory
import org.scalamock.scalatest.AbstractAsyncMockFactory
import org.scalatest.AsyncTestSuite

trait AsyncMockFactory extends AbstractAsyncMockFactory with ProxyMockFactory with AsyncTestSuite
trait AsyncMockFactory extends AbstractAsyncMockFactory with ProxyMockFactory { this: AsyncTestSuite =>

}
Expand Up @@ -29,4 +29,6 @@ import org.scalatest.TestSuite
*
* See [[org.scalamock]] for overview documentation.
*/
trait MockFactory extends AbstractMockFactory with ProxyMockFactory with TestSuite
trait MockFactory extends AbstractMockFactory with ProxyMockFactory { this: TestSuite =>

}
@@ -0,0 +1,43 @@
// Copyright (c) 2011-2015 ScalaMock Contributors (https://github.com/paulbutcher/ScalaMock/graphs/contributors)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package org.scalamock.test.scalatest

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalamock.scalatest.MockFactory
import org.scalamock.scalatest.AsyncMockFactory

/**
* Tests for issue #371
*/
class AsyncSyncMixinTest extends AnyFlatSpec {

"MockFactory" should "be mixed only with Any*Spec and not Async*Spec traits" in {
assertCompiles("new AnyFlatSpec with MockFactory")
assertDoesNotCompile("new AsyncFlatSpec with MockFactory")
}

"AsyncMockFactory" should "be mixed only with Async*Spec and not Any*Spec traits" in {
assertCompiles("new AsyncFlatSpec with AsyncMockFactory")
assertDoesNotCompile("new AnyFlatSpec with AsyncMockFactory")
}

}

0 comments on commit e8ab8f2

Please sign in to comment.