Skip to content

Commit

Permalink
Merge pull request #22 from NagRock/issue/12
Browse files Browse the repository at this point in the history
2.x version with new mocking and capturing API
  • Loading branch information
NagRock authored Apr 28, 2017
2 parents 6807f0f + e70c605 commit 09a157f
Show file tree
Hide file tree
Showing 20 changed files with 498 additions and 316 deletions.
98 changes: 48 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Mocking library for TypeScript inspired by http://mockito.org/

## 1.x to 2.x migration guide
[1.x to 2.x migration guide](https://github.com/NagRock/ts-mockito/wiki/ts-mockito-1.x-to-2.x-migration-guide)

## Main features


Expand All @@ -17,7 +20,7 @@ Mocking library for TypeScript inspired by http://mockito.org/
* `once`, `twice`, `times`, `atLeast` etc. - allows call count verification
* `calledBefore`, `calledAfter` - allows call order verification
* Resetting mock (`reset`, `resetCalls`)
* Capturing arguments passed to method (`thenCapture`)
* Capturing arguments passed to method (`capture`)
* Recording multiple behaviors
* Readable error messages (ex. 'Expected "convertNumberToString(strictEqual(3))" to be called 2 time(s). But has been called 1 time(s).')

Expand Down Expand Up @@ -210,85 +213,80 @@ console.log(foo.getBar(1)); // null - previously added stub has be
let mockedFoo:Foo = mock(Foo);
let foo:Foo = instance(mockedFoo);

// Create captor for all arguments
let firstArgCaptor: Captor<number> = new Captor<number>();
let secondArgCaptor: Captor<number> = new Captor<number>();

// Set matcher with anything() to capture all calls
when(mockedFoo.sumTwoNumbers(anything(), anything())).thenCapture(firstArgCaptor, secondArgCaptor);

// Call method twice with different values
// Call method
foo.sumTwoNumbers(1, 2);
foo.sumTwoNumbers(3, 4);

// Check first arg captor values
console.log(firstArgCaptor.getFirstCallValue()); // prints 1
console.log(firstArgCaptor.getLastCallValue()); // prints 3

// Check second arg captor values
console.log(secondArgCaptor.getFirstCallValue()); // prints 2
console.log(secondArgCaptor.getLastCallValue()); // prints 4
const [firstArg, secondArg] = capture(mockedFoo.sumTwoNumbers).last();
console.log(firstArg); // prints 1
console.log(secondArg); // prints 2
```
You can also capture single arg and give matcher to the other
You can also get other calls using `first()`, `second()`, `byCallIndex(3)` and more...
``` typescript
let mockedFoo:Foo = mock(Foo);
let foo:Foo = instance(mockedFoo);
### Recording multiple behaviors
// Create captor for second argument
let secondArgCaptor: Captor<number> = new Captor<number>();
You can set multiple returning values for same matching values
// Set matcher equal matcher (number === 3) for first arg and anything() for second
when(mockedFoo.sumTwoNumbers(3, anything())).thenCapture(new Captor(), secondArgCaptor);
``` typescript
const mockedFoo:Foo = mock(Foo);

// Call method twice with different values
foo.sumTwoNumbers(1, 2); // this call will not be captured becasue first arg !== 3
foo.sumTwoNumbers(3, 4);
when(mockedFoo.getBar(anyNumber())).thenReturn('one').thenReturn('two').thenReturn('three');

// Check second arg captor values
console.log(secondArgCaptor.getFirstCallValue()); // prints 4
console.log(secondArgCaptor.getLastCallValue()); // prints 4
const foo:Foo = instance(mockedFoo);

// As you can see first and last call values are same, because only second call has been captured
console.log(foo.getBar(1)); // one
console.log(foo.getBar(1)); // two
console.log(foo.getBar(1)); // three
console.log(foo.getBar(1)); // three - last defined behavior will be repeated infinitely
```
### Recording multiple behaviors
If more than one behavior is set, first matching is executed and removed
Another example with specific values
``` typescript
let mockedFoo:Foo = mock(Foo);

when(mockedFoo.getBar(anyNumber())).thenReturn('one');
when(mockedFoo.getBar(anyNumber()).thenReturn('two');
when(mockedFoo.getBar(anyNumber())).thenReturn('three');
when(mockedFoo.getBar(1)).thenReturn('one').thenReturn('another one');
when(mockedFoo.getBar(2)).thenReturn('two');

let foo:Foo = instance(mockedFoo);

console.log(foo.getBar(1)); // one
console.log(foo.getBar(2)); // two
console.log(foo.getBar(1)); // another one
console.log(foo.getBar(1)); // another one - this is last defined behavior for arg '1' so it will be repeated
console.log(foo.getBar(2)); // two
console.log(foo.getBar(2)); // two - this is last defined behavior for arg '2' so it will be repeated
```
Short notation:
``` typescript
const mockedFoo:Foo = mock(Foo);

// You can specify return values as multiple thenReturn args
when(mockedFoo.getBar(anyNumber())).thenReturn('one', 'two', 'three');

const foo:Foo = instance(mockedFoo);

console.log(foo.getBar(1)); // one
console.log(foo.getBar(1)); // two
console.log(foo.getBar(1)); // three
console.log(foo.getBar(1)); // null - no more behaviors defined
console.log(foo.getBar(1)); // three - last defined behavior will be repeated infinity
```
Another example with specific values
Possible errors:
``` typescript
let mockedFoo:Foo = mock(Foo);
const mockedFoo:Foo = mock(Foo);

when(mockedFoo.getBar(1)).thenReturn('one');
when(mockedFoo.getBar(1)).thenReturn('second time one');
when(mockedFoo.getBar(2)).thenReturn('two');
// When multiple matchers, matches same result:
when(mockedFoo.getBar(anyNumber())).thenReturn('one');
when(mockedFoo.getBar(3)).thenReturn('one');

let foo:Foo = instance(mockedFoo);
const foo:Foo = instance(mockedFoo);
foo.getBar(3); // MultipleMatchersMatchSameStubError will be thrown, two matchers match same method call

console.log(foo.getBar(1)); // one
console.log(foo.getBar(1)); // second time one
console.log(foo.getBar(1)); // null - no more behaviors for arg === 1 defined
console.log(foo.getBar(2)); // two
console.log(foo.getBar(2)); // null - no more behaviors for arg === 2 defined
```
### Thanks
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-mockito",
"version": "1.2.0",
"version": "2.0.0",
"description": "Mocking library for TypeScript",
"main": "lib/ts-mockito.js",
"typings": "lib/ts-mockito",
Expand Down
27 changes: 0 additions & 27 deletions src/Captor.ts

This file was deleted.

54 changes: 35 additions & 19 deletions src/MethodStubCollection.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,67 @@
import {MethodStub} from './stub/MethodStub';
import {MethodStub} from "./stub/MethodStub";

export class MethodStubCollection {
private hadMoreThanOneBehavior: boolean = false;
private items: Array<MethodStub> = [];
private items: MethodStub[] = [];

public add(item: MethodStub) {
this.items.push(item);
}

if(this.items.length > 1) {
this.hadMoreThanOneBehavior = true;
public getLastMatchingGroupIndex(args): number {
for (let i = this.items.length - 1; i >= 0; i--) {
const item = this.items[i];
if (item.isApplicable(args)) {
return item.getGroupIndex();
}
}
return -1;
}

public getFirstMatchingAndRemove(args): MethodStub {
let index = this.getFirstMatchingIndex(args);
let result = this.getFirstMatching(args);
if (index > -1) {
public getFirstMatchingFromGroupAndRemoveIfNotLast(groupIndex: number, args: any[]): MethodStub {
let index = this.getFirstMatchingIndexFromGroup(groupIndex, args);
let result = this.getFirstMatchingFromGroup(groupIndex, args);
if (index > -1 && this.getItemsCountInGroup(groupIndex) > 1) {
this.items.splice(index, 1);
}
return result;
}

public getFirstMatching(args): MethodStub {
private getFirstMatchingFromGroup(groupIndex: number, args: any[]): MethodStub {
for (let item of this.items) {
if (item.isApplicable(args)) {
if (item.getGroupIndex() === groupIndex && item.isApplicable(args)) {
return item;
}
}
return null;
}

public getHadMoreThanOneBehavior(): boolean {
return this.hadMoreThanOneBehavior;
}

public hasMatching(args): boolean {
return this.getFirstMatchingIndex(args) > -1;
public hasMatchingInAnyGroup(args: any[]): boolean {
for (let item of this.items) {
if (item.isApplicable(args)) {
return true;
}
}
return false;
}

private getFirstMatchingIndex(args): number {
private getFirstMatchingIndexFromGroup(groupIndex: number, args: any[]): number {
let index = 0;
for (let item of this.items) {
if (item.isApplicable(args)) {
if (item.getGroupIndex() === groupIndex && item.isApplicable(args)) {
return index;
}
index++;
}
return -1;
}

private getItemsCountInGroup(groupIndex: number): number {
let result = 0;
for (let item of this.items) {
if (item.getGroupIndex() === groupIndex) {
result++;
}
}
return result;
}
}
38 changes: 21 additions & 17 deletions src/MethodStubSetter.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import {MethodToStub} from './MethodToStub';
import {ReturnValueMethodStub} from './stub/ReturnValueMethodStub';
import {ThrowErrorMethodStub} from './stub/ThrowErrorMethodStub';
import {CallFunctionMethodStub} from './stub/CallFunctionMethodStub';
import {Captor} from './Captor';
import {CaptorMethodStub} from './stub/CaptorMethodStub';
import {MethodToStub} from "./MethodToStub";
import {ReturnValueMethodStub} from "./stub/ReturnValueMethodStub";
import {ThrowErrorMethodStub} from "./stub/ThrowErrorMethodStub";
import {CallFunctionMethodStub} from "./stub/CallFunctionMethodStub";

export class MethodStubSetter<T> {
constructor(private methodToStub: MethodToStub) {

}
private static globalGroupIndex: number = 0;
private groupIndex: number;

public thenReturn(value: T): void {
this.methodToStub.methodStubCollection.add(new ReturnValueMethodStub(this.methodToStub.matchers, value));
constructor(private methodToStub: MethodToStub) {
this.groupIndex = ++MethodStubSetter.globalGroupIndex;
}

public throwsError(error: Error): void {
this.methodToStub.methodStubCollection.add(new ThrowErrorMethodStub(this.methodToStub.matchers, error));
public thenReturn(...rest: T[]): this {
for (let value of rest) {
this.methodToStub.methodStubCollection.add(new ReturnValueMethodStub(this.groupIndex, this.methodToStub.matchers, value));
}
return this;
}

public thenCall(func: (...args:any[]) => any): void {
this.methodToStub.methodStubCollection.add(new CallFunctionMethodStub(this.methodToStub.matchers, func));
public thenThrow(...rest: Error[]): this {
for (let error of rest) {
this.methodToStub.methodStubCollection.add(new ThrowErrorMethodStub(this.groupIndex, this.methodToStub.matchers, error));
}
return this;
}

public thenCapture(...args:Captor<any>[]):void {
this.methodToStub.methodStubCollection.add(new CaptorMethodStub(this.methodToStub.matchers, args));
public thenCall(func: (...args: any[]) => any): this {
this.methodToStub.methodStubCollection.add(new CallFunctionMethodStub(this.groupIndex, this.methodToStub.matchers, func));
return this;
}
}
17 changes: 10 additions & 7 deletions src/Mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {PrototypeKeyCodeGetter} from "./utils/PrototypeKeyCodeGetter";

export class Mocker {
private methodStubCollections: any = {};
private methodActions: Array<MethodAction> = [];
private methodActions: MethodAction[] = [];
private mock: any = {};
private instance: any = {};
private redundantMethodNameInCodeFinder = new RedundantMethodNameInCodeFinder();
Expand Down Expand Up @@ -244,13 +244,16 @@ export class Mocker {
private getMethodStub(key, args): MethodStub {
let methodStub: MethodStubCollection = this.methodStubCollections[key];
if (!methodStub) {
return new ReturnValueMethodStub([], null);
} else if (methodStub.getHadMoreThanOneBehavior() && methodStub.hasMatching(args)) {
return methodStub.getFirstMatchingAndRemove(args);
} else if (methodStub.hasMatching(args)) {
return methodStub.getFirstMatching(args);
return new ReturnValueMethodStub(-1, [], null);
} else if (methodStub.hasMatchingInAnyGroup(args)) {
const groupIndex = methodStub.getLastMatchingGroupIndex(args);
return methodStub.getFirstMatchingFromGroupAndRemoveIfNotLast(groupIndex, args);
} else {
return new ReturnValueMethodStub([], null);
return new ReturnValueMethodStub(-1, [], null);
}
}

getActionsByName(name: string): MethodAction[] {
return this.methodActions.filter(action => action.methodName === name);
}
}
Loading

0 comments on commit 09a157f

Please sign in to comment.