Open gunhaxxor opened 2 years ago
You're in a difficult spot when learning object-oriented programming in a Javascript environment because even with Typescript (which introduces interface-like constructs) you don't have traditional interfaces: the interface you're mocking with this library isn't a traditional interface. In traditional object-oriented programming, an interface represents a contract for behaviour that an object must comply with. A property (sometimes called a member
) is not a behaviour, and so it is not part of the contract for behaviour: therefore, in most languages, it is not possible to have a property on an interface... and as a consequence, an object that implements the contract would not use properties because they're not a part of the contract it depends on.
Personally, I lean towards strict adherence to the principles of good interface design, even when working in Javascript: I do not use properties on any implementation of an interface. That said, there's some degree of pragmatism required when writing code, especially when it comes to testing: principles are great, but they're only useful when they enable you to be more effective, strict adherence to principles for principle's sake is a mistake...
So, what you've described is wrong and it would not work in some other languages, and this library is pointing you in a good direction: methods for everything that belongs in an interface... but it's not wrong, for you as a developer, if it makes you more effective :-)
Thanks for the work on this project. Like the original poster, I'm trying to understand how to use this library to mock an interface with properties.
I need to mock the HttpRequest
interface from the Microsoft Azure functions package. The interface is defined here by a bunch of properties:
/**
* HTTP request object. Provided to your function when using HTTP Bindings.
*/
export interface HttpRequest {
/**
* HTTP request method used to invoke this function.
*/
method: HttpMethod | null;
/**
* Request URL.
*/
url: string;
/**
* HTTP request headers.
*/
headers: {
[key: string]: string;
};
/**
* Query string parameter keys and values from the URL.
*/
query: {
[key: string]: string;
};
/**
* Route parameter keys and values.
*/
params: {
[key: string]: string;
};
/**
* The HTTP request body.
*/
body?: any;
/**
* The HTTP request body as a UTF-8 string.
*/
rawBody?: any;
}
The Microsoft documentation for Typescript interfaces includes properties.
Since this project is about mocking Typescript interfaces it seems like including properties is essential functionality.
What is your suggestion for how to mock interfaces like this?
(Just in case it's unclear, I am not the author of this library, just another user!)
What is your suggestion for how to mock interfaces like this?
@cberthiaume That would depend on the behaviour you're trying to test. As in my previous comment, speaking about generalised object-oriented principles: while there are cases where you need to verify the interaction between your own code and a third-party library, they are uncommon and you should try to avoid mocking anything that interacts with third-party code. For example, if you wrote a test that said "the Azure functions package should read the value url
from my HttpRequest
once" and then in a later version Azure functions is updated to read url
twice because of some additional logic to validate the input, your tests will fail unexpectedly.
Rather than mocking, you could create a simple object that is compatible with the HttpRequest
interface by providing only the required values for the method
and url
.
const exampleRequest: HttpRequest = {
method: "GET"
url: "https://example.com/something",
}
If this doesn't meet your needs, can you speak more about your use case? There is a functional workaround described in https://github.com/marchaos/jest-mock-extended/issues/4#issuecomment-587446452 that will allow you to mock properties so it's certainly possible if you need it, but as mentioned, I suspect it's not the right solution to what your end goal is.
Thanks for the response. I'm sure I'm doing something wrong.
My use case is that I have a method in my code that accepts an HttpRequest
and uses the body
from it. If I create a simple object like:
const request = { body: "test body" };
I get a linter error:
TS2345: Argument of type '{ body: Buffer; }' is not assignable to parameter of type 'HttpRequest'. Type '{ body: Buffer; }' is missing the following properties from type 'HttpRequest': method, url, headers, query, params
I can add those properties to my simple object but that's one thing I expect a mock library to do for me.
I can suppress the linter error with @ts-ignore
but I'd rather not.
I'm sure I'm doing something wrong [...] I can add those properties to my simple object but that's one thing I expect a mock library to do for me.
@cberthiaume There's rarely right and wrong, just different ways of doing things! Everything I've said here is my opinion, not an absolute truth. There's a great deal of nuance and divided opinion around Mock vs. Stub vs. Fake vs. Spy, but in general, a Mock is a specific type of Test Object that is used to test the behaviour of the Subject Under Test. Often people will use "Mock" and "Test Object" interchangeably, which is very confusing.
The use-case I think you're describing is the desire to write the least amount of code to create a Test Object: defining only the properties your Subject Under Test cares about reading. That type of Test Object is usually called a Stub, which is a Test Object that provides a fixed set of output (through properties, or behaviour) for the Subject Under Test to use.
I can't speak for the author, but as a user, I consider this library (jest-mock-extended
) to be using Mock
in the true sense: to refer to the specific type of Test Object used for testing and verifying behaviour. The pain you're experiencing is because you're trying to use a Mocking library for Stubbing: it's certainly possible, but not usually the best way.
Personally, I don't use a library for Stubs. Even if it can feel verbose, I create real objects (inline in the test, or using a Test Data Builder or an Object Mother depending on circumstance). So, this is all a long-winded way of saying that, despite how verbose it feels, I think you'd be best off doing this:
const exampleRequest: HttpRequest = {
method: "GET"
url: "https://example.com/",
body: new Buffer("the body I want to test"),
}
If you are interested in more of the nuance and a much better description than I've provided here, I highly recommend Martin Fowler's Mocks Aren't Stubs.
You're correct about mock/stub distinction. I'm being too loose with my language. I'm also used to mocking libraries supporting easy stubbing as well.
Thanks again for the assistance.
I appreciate the detail in the explanation, but I don't think mocks and stubs is a particularly necessary distinction here, nor are general OO principles which don't, as you say, apply to TS to begin with?
This problem exists even if you're doing straight FP, btw. (And then there's no conceptual distinction between a value that is a constant and one produced through applying a function expression.)
Theory aside, the use case is pretty clear: the mock should evaluate to specified values where we want to specify those values. Whether it's through properties, functions, or anything else doesn't really matter as such.
Im late for two years now, but the thing is this "interface" is rather.... just a type:) Even if it called "interface" in terms of TS, it is actually just a type and describes "plain" structure-object. To be clear: we can defined those as "interface" or "type" directly, the difference is in usage. (for example, you can combine interfaces by using "implements", etc.)
I'm rather new to testing in general and using jest in particular.
I'm curious about if there is a preferred/correct way to handle properties or class fields when mocking interfaces? To me it seems that all members of an interface are created as mockingfunctions, whether the type is a function or not? In my case I want to mock an id property of a class instance. It seems I could do something like this:
Is this for some reason discouraged or "wrong". I want to avoid any pitfalls that could come back and hunt me later. Thankful for any help!