architecture overview
This commit is contained in:
Родитель
c064a9d5c6
Коммит
617cae8fbf
|
@ -0,0 +1,104 @@
|
|||
# JustMock Developer Overview
|
||||
|
||||
JustMock is a compact, yet highly complex library. The following should at least provide some insight about the structure of the library and the some of the design decisions behind it.
|
||||
|
||||
Unlike other mocking libraries out there, JustMock must be able to function in the context of the JustMock profiler and the notion that everything, even basic stuff like `Enumerable.FirstOrDefault()` can have its behavior changed. The design of JustMock goes to great lengths to ensure that it can mock everything without pulling the rug from under its feet.
|
||||
|
||||
## Licensing
|
||||
JustMock Lite is licensed under the Apache 2.0 OSS license. New third-party dependencies must not be used in violation of their license given this fact.
|
||||
|
||||
## Organization
|
||||
|
||||
At the top level JustMock is a single assembly. Having a single assembly for everything simplifies versioning and deployment. The assembly itself is broadly separated into two parts - the public API and the core implementation.
|
||||
|
||||
In principle, the core implementation is not tied to a particular frontend, or a particular form of the public API. In principle, any public API, for example, Moq's public API, can be re-implemented in terms of the primitives provided by the core. In practice, this is not possible, because there are always subtle semantic differences among libraries, so it's impossible to use a single core implementation and say that JustMock can be turned into a drop-in replacement for any mocking library just by reimplementing the public API. Yet, the design of JustMock is based on a separation between the core primitives and the form of the public API. There are some abstraction leakages (like Behavior - a public API component, leaking into the core), which could be eliminated with better design and refactoring.
|
||||
|
||||
The core implementation is contained in the `Core/` folder. Everything outside the Core folder is the friendly part of the public API of JustMock. The public API has very little functionality in itself and delegates almost everything to the Core.
|
||||
|
||||
Even though JustMock is a single assembly, it has several dependencies that arecompiled into it. The biggest are Castle.DynamicProxy and NInject. Essentially, the original projects were forked some time ago, stuffed into internal JustMock namespaces and made part of the JustMock assembly. This decision has some advantages and drawbacks:
|
||||
* If the test project also depends on Castle or NInject, then there could be no version conflict - both versions of the dependency can work side-by-side. We had a problem where JustMock was tied to Unity 2, but the developer used Unity 3 in their project and the two assemblies couldn't work together.
|
||||
* Bugfixes from the original project have to be back-ported - it's no longer a simple replacement of the dependent assembly.
|
||||
* Plugins designed to work with either library no longer work, because the namespaces are changed to avoid conflicts.
|
||||
|
||||
## Architecture and Terminology
|
||||
|
||||
JustMock's code base takes care of three broad categories - contexts, calls and objects.
|
||||
|
||||
### Mocking contexts
|
||||
|
||||
Arrangements have a lifetime. In regular mocking libraries the lifetime of an arrangement is usually the same as the lifetime of the instance on which the arrangement is made. The lifetime of the mock instance is managed by the user - they can make a fresh instance for every test, or they can reuse an instance and its arrangements across tests. JustMock has a harder problem to deal with - not every arrangement has an associated instance, or the instance is beyond the user's control. The simplest example is static methods. At what point should the arrangement on `DateTime.Now` expire?
|
||||
|
||||
Static and future arrangements have no natural context to tie the lifetime, unlike instance arrangements. Therefore, a new context source has to be devised. The context source that JustMock uses is the unit testing framework itself. Methods and types decorated with attributes of a unit testing framework define the context within which arrangements are active. See the [isolation document](Isolation.md) for more information about the algorithm used to define the lifetime of arrangements and their isolation between tests.
|
||||
|
||||
The core type which ties together context management and discovery is `Core.Context.MockingContext`. At any point in the application, accessing the `MockingContext.CurrentRepository` or `MockingContext.ResolveRepository` members will create the necessary stack of mock repositories defining the current behavior of JustMock within the current context. The entry point for the JustMock public API like `Mock.Create` and `Mock.Arrange` is one of the methods on this type.
|
||||
|
||||
A "repository" or "mocks repository" is implemented by the `Core.MocksRepository` type. Its job is creating, arranging and asserting mocks. The `MockingContext`'s job is to create and retire mocks repositories as the context changes.
|
||||
|
||||
The `MockingContext` class depends on `IMockingContextResolver` and its implementers to resolve the current context. There are `IMockingContextResolver` implementers for different unit testing frameworks, like MSTest and NUnit, each being able to build the mocks repository with the correct accumulated context depending on knowledge about the design of each unit testing framework.
|
||||
|
||||
### Objects
|
||||
In regular mocking libraries, the equivalent of the `Mock.Create<T>` method is the user's entry point into the library. All library operations can only be applied to a mock created by that library. JustMock is different. Its profiler allows it to replace the behavior of any method, on any object, or even when there is no object (static methods).
|
||||
|
||||
In the absence of abstract classes and interfaces, JustMock would not even need a `Mock.Create<T>` method. You can just create regular instances and arrange their behavior. In fact, this is exactly what JustMock's `Mock.Create<T>` does for non-abstract types when the profiler is enabled - it simply instantiates the object (optionally calling the object constructor) and intercepts all calls to it. This is exactly how sealed classes are mocked.
|
||||
|
||||
Still, JustMock needs to account for the existence of interfaces and abstract classes, and also for the use case where the profiler is not enabled. The non-portable build of JustMock handles those cases using the Castle.DynamicProxy library. The library is interned in `Core/DynamicProxy/`. The library's duty is to build at runtime subclasses or interface implementations of a given type and allow you to intercept all calls to virtual members on that type. The creation is handled by the `Core.DynamicProxyMockFactory` class. Interception is handled by the `Core.DynamicProxyInterceptor` class.
|
||||
|
||||
Mocks of `MarshalByRef` objects are handled differently. Their mocks are in fact transparent proxies and are created by `Core.TransparentProxy.MockingProxy`. The use of transparent proxies allows JustMock to mock all public members on a MarshalByRef object, even without the profiler.
|
||||
|
||||
Every object touched by JustMock has a corresponding `Core.IMockMixin` that specifies the behavior of that object when calls to it are intercepted. Objects created through a DynamicProxy have a built-in `IMockMixin` which is accessed through a simple cast. Objects that were not created by JustMock, as well as static methods, have a IMockMixin object stored in a look-aside table. The general algorithm for recovering a `IMockMixin` from any object or type is implemented in `MocksRepository.GetMockMixin`. There is also the `Core.Invocation.MockMixin` property based on the previous method which gives you the mock mixin associated with the currently intercepted invocation. It is easier to work with this property whenever code is executing in the context of an invocation.
|
||||
|
||||
The general algorithm of creating a mock object is encoded in `MocksRepository.Create`.
|
||||
|
||||
### Calls to intercepted methods
|
||||
At the heart of a mocking library is the ability to specify replacement behavior for certain calls and then being able to intercept those calls to execute the replacement behavior. Interception begins in either `Core.DynamicProxyInterceptor`, `Core.ProfilerInterceptor` or `Core.TransparentProxy.MockingProxy`. Note that transparent proxies delegate to DynamicProxyInterceptor, but use a custom implementation of `Core.Castle.DynamicProxy.IInvocation`. When a call is intercepted by any of the above interceptors, its first job is to determine the current MocksRepository. It can do that by looking up the current repository in the MockingContext or by taking the repository with the `IMockMixin` associated with the current instance or type. If a MocksRepository is found, a `Core.Invocation` object is constructed and `MocksRepository.DispatchInvocation` is called. The `Invocation` object stores all the input state describing what method was called and what arguments were passed, as well as all output state, like what the return value should be and if the original implementation should be skipped. After the dispatch completes, the interceptor examines the output state and returns it back to the calling method or passes to the original implementation.
|
||||
|
||||
The specification or replacement behavior in JustMock is called an "arrangement". Every call to `Mock.Arrange` produces a single arrangement in the current context's MocksRepository.
|
||||
|
||||
A single arrangement is represented in the core by a `Core.IMethodMock` object and is called a "method mock". `Mock.Arrange` creates instances of `Expectations.ActionExpectation` and `Expectations.FuncExpectation` which implement `Core.IMethodMock`. The object returned by `Mock.Arrange` also implements the API interfaces in `Expectations/Abstractions/`. These interfaces allow for the fluent configuration of arrangements with calls like `.DoInstead()`, `.Returns()` or `.OccursOnce()`. These fluent calls are called "clauses" in general. Each of these clauses attaches a specific "behavior" the method mock.
|
||||
|
||||
Not all intercepted calls might be linked to an arrangement. A generic behavior can be specified when creating mocks with `Mock.Create<T>` or `Mock.SetupStatic()`, like Behavior.Loose or Behavior.Strict. This generic behavior specifies the behavior of method calls which are not linked to an arrangement. Whenever a call is intercepted, JustMock first searches for a corresponding method mock, and if one is not found, it delegates to the generic behavior associated with that mock.
|
||||
|
||||
#### Behaviors
|
||||
|
||||
In both cases behaviors are represented as implementers of `Core.Behaviors.IBehavior`. A method mock has the properties `IMethodMock.Behaviors` and `IMethodMock.OccurrencesBehavior`. A mock mixin has the properties `IMockMixin.FallbackBehaviors` and `IMockMixin.SupplementaryBehaviors`. JustMock searches for a corresponding method mock when processing an intercepted call. If one is found, then its set of behaviors is used to define the behavior of the call. If no method mock is found, the mock mixin for the intercepted object or type is looked up and its `FallbackBehaviors` are executed. If a mock mixin is available, then its `SupplementaryBehaviors` are always executed at the end of an interception. If no mock mixin and no method mock are found, execution continues to the method's original implementation.
|
||||
|
||||
`MockBuilder.DissectBehavior` is responsible for turning a value from the `Behavior` enum into a list of supplementary and fallback behaviors to be passed to `MocksRepository.Create`.
|
||||
|
||||
All behavior implementations are found in `Core/Behaviors/`. Some behaviors only make sense on a method mock, others only on a mock mixin, still others make sense on both.
|
||||
|
||||
Behaviors use the output state of the `Invocation` object passed to `IBehavior.Process` to communicate among themselves if necessary.
|
||||
|
||||
#### Call patterns and invocations
|
||||
Invocation objects always contain concrete instances and concrete arguments. Yet, users can specify arbitrary argument predicates in their arrangements. For example, the might specify that the arrangement should only be executed if an argument has a certain value, or matches a certain predicate, or that an argument's value doesn't matter. These predicates are expressed in terms of matchers. Matchers are implementations of `Core.MatcherTree.IMatcher`. Users specify matchers using the methods on the `Arg` and `ArgExpr` classes.
|
||||
|
||||
The entire expression passed to `Mock.Arrange` and its variants is called a "call pattern". Every invocation can be checked to see if it matches a given call pattern. JustMock converts call patterns passed to `Mock.Arrange` into `Core.CallPattern` objects and tests invocations against the, to find corresponding method mocks. The relevant methods are `MocksRepository.ConvertActionToCallPattern`, `MocksRepository.ConvertExpressionToCallPattern` and `MocksRepository.ConvertMethodInfoToCallPattern`, used by `Mock.ArrangeSet`, `Mock.Arrange` and `Mock.NonPublic.Arrange` respectively.
|
||||
|
||||
Method mocks and their call patterns are stored in a dictionary of trees - `MocksRepository.arrangementTreeRoots`. The key in the dictionary and the base of each tree is the intercepted member. The Nth level of the tree corresponds to the Nth argument of the method. The tree itself is implemented by `Core.MatcherTree.IMatcherTreeNode` and its implementers. Every non-leaf tree node contains the matcher for the corresponding argument in the call pattern. The leafs are instances of `Core.MatcherTree.MethodMockMatcherTreeNode` and contain the `IMethodMock` whose call pattern was used to construct this path in the matcher tree. Invocation dispatch starts at the base of the tree corresponding to the intercepted method and finishes at a leaf that provides the method mock. The matcher at each level is tested against the corresponding argument in the invocation to determine whether this branch of the tree should be checked further. If no leaf is found, this means that no method mock corresponds to the intercepted invocation and control is passed to the generic behavior specified by the mock mixin.
|
||||
|
||||
### Assertions
|
||||
Arrangement clauses that specify expected behavior are called "expectations" and their associated behavior implements `Core.Behaviors.IAssertableBehavior`. Users call `Mock.Assert` to assert that an expectation is fulfilled. There are several flavors of expectations.
|
||||
|
||||
#### Method mock expectations
|
||||
Expectations given in arrangement clauses are stored in the method mock of that arrangement. These can be asserted by calling `Mock.Assert(object)` and `Mock.Assert(type)`. This form of assertion finds all method mocks for that object or type, i.e. all methods arranged for that object and type and assert all assertable behaviors in that method mock. In addition, all behaviors on the associated mock mixin are asserted and also all behaviors on dependent mocks. Dependent mocks are stored in `IMockMixin` and are added for example by arranging a call on a mock to return another mock. See `IMockMixin.DependentMocks`.
|
||||
|
||||
#### Explicit occurrence expectations
|
||||
Calls to `Mock.Assert(expression, ...)` work differently, depending on whether `expression` matches an existing arrangement and is a bit of a mess. The following description might not be accurate in all possible permutations of arguments passed to `Mock.Assert` and pre-existing arrangements. If `expression` corresponds to an arrangement, then the corresponding method mock is asserted.
|
||||
|
||||
If `expression` does not correspond to an arrangement or an `Occurs` is given, then the number of invocations matching the given call pattern are counted in the `MocksRepository.invocationTreeRoots` tree and the number of occurrences is asserted. If an `Occurs` argument is not given, then JustMock checks that at least one invocation matching the call pattern has occurred, otherwise it asserts the count specified by `Occurs`.
|
||||
|
||||
JustMock has a very peculiar behavior when the call pattern of a method mock uses non-trivial argument matchers and the call pattern built from `expression` is also non-trivial. The details are implemented around the `Core.MatcherTree.MatchingOptions` enum, the code that uses it, and the logic used to compute whether one matcher matches another.
|
||||
|
||||
#### Implicit expectations
|
||||
A call to `Mock.AssertAll(object)` asserts all expectations just like `Mock.Assert(object)`. In addition, it treats all arrangements on the given object as `.MustBeCalled()` unless an explicit occurrence expectation is specified.
|
||||
|
||||
## Profiler isolation
|
||||
The JustMock profiler allows you to mock almost anything. You could, for example arrange `List<object>.Add(o)` to do nothing for all list instances. Obviously, if JustMock tries to use a method arranged unexpectedly, it could fail spectacularly. On the other hand, JustMock should not limit the user's ability to mock anything, even if JustMock depends on the original implementation.
|
||||
|
||||
Isolation from the effects of possibly disrupting arrangements is achieved using the `ProfilerInterceptor.GuardInternal` and `ProfilerInterceptor.GuardExternal` methods. For starters, the profiler never instruments any method in the Telerik.JustMock assembly. Also, every public JustMock method's body is wrapped in a call to `ProfilerInterceptor.GuardInternal`. `GuardInternal` disables all interception for all code executing inside it. Calls to `GuardInternal` can safely be nested. So, even if the user arranges a method used internally by JustMock, all JustMock code executes in `GuardInternal` blocks in which no calls are ever intercepted. Whenever JustMock needs to call back into user code, e.g. call the delegate passed to a `.DoInstead()`, the delegate is executed in a `GuardExternal` block. `GuardExternal` re-enables all interception for all code executing inside it. `GuardInternal` and `GuardExternal` blocks can be freely mixed. If code is executing directly under a `GuardInternal` call, then interception is disabled. If code is executing directly under a `GuardExternal` call, then interception is enabled. When exiting a block, guard state is managed so that interception always behaves with respect only to the inner-most `GuardInternal` or `GuardExternal` on the call stack.
|
||||
|
||||
A common source of bugs is calling user code directly, without wrapping it in a `GuardExternal` block. In those cases user code will not see the effect of their existing arrangements. One important thing to note is that JustMock should execute **only** user code in `GuardExternal` block. For example, if JustMock needs to call a method with a dynamic signature, then it cannot use `MethodBase.Invoke` in a `GuardExternal` block, because the behavior of `MethodBase.Invoke` or any of the methods used in its implementation may be arranged. In such cases JustMock relies on `MockingUtil.MakeFuncCaller` to create a dynamic method that takes an array of arguments and can call a delegate with any signature. `MakeFuncCaller` is called in the current `GuardInternal` block and the result delegate can be safely called in a `GuardExternal` block.
|
||||
|
||||
There is a test: `ImplementationCorrectnessTests.AssertAllPublicMethodsAreWrappedInGuardInternal`, that asserts that the bodies of all non-trivial public methods in the JustMock API are wrapped in calls to GuardInternal.
|
||||
|
||||
## Debugging tests
|
||||
JustMock has a useful debugging facility implemented by `Telerik.JustMock.DebugView`. This facility is geared towards users, not developers. It provides an insight into JustMock's inner workings and decision making process. Advanced users can depend on it to understand why JustMock behaves the way it does. The class documentation gives a full tutorial how to use it. Occasionally relying on this type is useful when dithering over support tickets. Sometimes, it's also much quicker to read the DebugView output rather than trace JustMock's execution in the debugger.
|
Загрузка…
Ссылка в новой задаче