11 stubs - ranhs/soda-test GitHub Wiki
We will use the same method as a sample for this section too:
export function foo() {
// some operation
console.log('console.log was called')
console.warn('console.warn was called')
return
}
Stubs are similer to spies, but they also allow you to change the hehaviour of the method you stub. In this case we will change the the console.warn so it will do something else.
The format of the stub decorator is the same as spy decorator. There are 2 arguments: the object were the method to stub is on, and the name of the method to stub.
@it('should stub console.warn')
warnStub( @stub(console, 'warn') console_warn_stub: SinonStub ): TR {
console_warn_stub.callsFake(() => console.log('message from sub'))
foo()
expect(console_warn_stub).to.have.been.calledOnce
expect(console_warn_stub).to.have.been.calledWith('console.warn was called')
}
Note that the console_warn_stub argument can be used to change the behavour of the warn method, in this example to log a differnt message to console.
However soda-test gives you a different way to define what the stub bevour is. After stating the stub decorator you can call on its return value (inline) to methods that defines its behaviour. For expmple, use the method calls to replace the stub method with a differnt one.
@it('should stub console.warn')
warnStub( @stub(console, 'warn').calls(() => console.log('message from sub')) console_warn_stub: SinonStub ): TR {
foo()
expect(console_warn_stub).to.have.been.calledOnce
expect(console_warn_stub).to.have.been.calledWith('console.warn was called')
}
Both test-steps above do the same thing. its changes the bevhour of the console.warn to log a differnt method, and validate the console.warn was called with with the expected argument. The output looks like this:
console.log was called
message from sub
√ should stub console.warn
Note that you can see the log message of foo, and the message that the warn-stub has logged, but not the original warn message, that was validated as part of the test-step code.
As in spy, you can define stub on context or class level by defining a member variable of type SinonStub and add the stub decorator to it.
@stub(console, 'warn').calls(() => console.log('message from sub'))
console_warn_stub: SinonStub
@it('should stub console.warn')
warnStub(): TR {
foo()
expect(this.console_warn_stub).to.have.been.calledOnce
expect(console_warn_stub).to.have.been.calledWith('console.warn was called')
}
Note that when you define a stub in the global context area, it will be available (and active) in each test-step in the class. If you define the stub in a context area, it will be avaiable (and active) for each test-step in that context, but not in test-steps of other contexts. Note that the stub is cleared/reset automaticly after each test-step it is active on.
Normaly a stub is define on an exiting method. You can define it in one of the follwoing ways:
To define the stub on a method that exists on an object, pass 2 arguments to the stub decorator:
- The object that the method exists on it
- The name of the method to stub
@stub(console, 'warn')
console_warn_stub: SinonStub
To define the stub on a method that is defined in a public libaray, pass 2 argumetns to the stub decorator:
- The name of the libaray the method exists on it
- The name of the method to stub
@stub('lodash', 'isString')
stubIsString: SinonStub
To define the stub on the method that is the library itself, or the default method of the libraray, pass to the stub decorator 1 argument:
- The name of the libaray that is the method to stub, or that has the default method to stub
@stub('parse-function')
parseFuncStub: SinonStub
Note: to import the library itself (that might be a method):
import * as parseFunction from 'parse-function'
to import the default libraray method:
import parseFunction from 'parse-function'
To define the stub on a method that is defined in a model in my project (whether the method is exported or not), pass 2 arguments to the stub decorator:
- Related path the the model the method exists on, as it is stated in import statement
- The name of the method to stub
@stub('./lib', 'squar')
stubSquar: SinonStub
To stub a member method of class (even if abstract method), pass 3 arguments to the stub decorator:
- The name of the public model where the class define on, or the relative path to the model in the currect poject where the class is defined
- The name of class
- The name of the class member method
@stub('./lib','DummyClass', 'Value')
valueStub: SinonStub
To stub the super class constructor, pass 3 argments to the stub decorator:
- The name of the public model where the class define on, or the relative ptath to the model in the current project where the class is defined
- The anme of the (derived) class
- "super"
@stub(DerivedClass,'super')
BaseClassStub: SinonStub
@it('should stub the call to super class constructor')
testStub(): TR {
BaseClass.constructorCount = 0
new DerivedClass()
expect(this.BaseClassStub).to.have.been.called
expect(BaseClass.constructorCount).to.equal(0)
}
You may deine a stub varible, without connecting it to any method (at lease when defining it). don't pass any arguments to the stub decorator.
@stub()
userSaveStub: SinonStub
The stub may be connected to a method latter on in code, or in as part of a different definition.
When defining a stub, you get a variable (or argument) of type SinonStub. This is a variable created by the sinon libraray. It can be used to call the stub as a method, and you can use it to define what the stub is doing (e.g. the method callsFake. However soda-test gives a way to define the action of the stub when it is decleared.
When you use the stub decorator, after passing it its argument, you can call a method, inline, on its return value to specify that it is done. By default the stub does nothing. Following are the options of defining what the stub does that soda-test is supporting:
To define a stub that return a value that is known in desing time (e.g. const value) you need to call on the stub decorator return value the method returns with the argument of the value you whats the stub to return.
@stub('./lib', 'double').returns(-101)
stubDouble: SinonStub
If the stubbed method returns a Promise, and you want the stub method to return a resolved promise, you can on the stub decorator return value to call the resolves method with the value you what it to resolve to
@stub('./users', "createUser").resolves({name:'foo'})
createStub: SinonStub
If the stubbed method returns a Promise, and you want the stub method to return a rejected promise, you can on the stub decorator return value to call the rejects method wit the Error object you what it to reject with. You may also pass a string to the rejects method, it this case the stub will return a Promise that is rejected with an Error object that its message is the string passed.
@stub('./users', "createUser").rejects('fake_error')
createStub: SinonStub
The previous options assume you have the value to return/resolve/reject at design time. But if you need the stub to do actions, or you what to return a value known only in run time, you need to make the stub run code. On the stub decorator return value, call the calls method with a function as argument. Note that this function should have the same signiture as the method you are stubbing.
@stub(User, 'findById').calls((id, callback)=>callback(null,sampleUser))
findStub1: SinonStub
If you stub a constructor method. You stub the class itself, since the name of the class is a name of a Function that is the class constructor. You want this method to return the new object, that has method in it that need to be stubs themselves. On the stub decorator return value, call the construct method passing it an object with properties that are the name name of the stub methods in the returned object, and the value of stub property is the name of a stub variable or argument defined before that stub that is using the construct method.
@stub().resolves(sampleUser)
saveStub: SinonStub
@stub('./models/user', 'User').construct({save: "saveStub"})
FakeUserClass: SinonStub
In the above example the class User defined in './models/user' model, is stubbed with a dummy constractor that returns an object that has the save method that is also a stub that returns a Promise that is resolved to an object that defined earlier called sampleUser
You might what to stub a getter or setter method. To do that you create a stub to the getter/setter name as if it was a regular method of that name. On the stub decorator return value you can call the access methods that gets 2 optional arguments.
- A value or a function. If a value, the getter method will return this value (when using the stub). if a function that getter will call this function, and return that it returns.
- A function that is called when the setter is called. The function gets an argument that is the value to set
@stub('./lib', 'BaseClass', 'Value').access(-1)
valueStub: SinonStub
@stub('./lib','DummyClass', 'Value').access(()=>4)
valueStub: SinonStub
@stub('./lib','DummyClass', 'Value').access(undefined,(v: number)=>{TestTest._v = v})
valueStub: SinonStub