How to test a custom Jasmine matcher? - jasmine

I created some custom matchers for Jasmine 2.x for which I want to create specs.
Most programmers test their matchers by just applying them in a test an verify if the outcode is positive.
For example the .toEqual 'custom' matcher:
it('should test if two objects are equal', function () {
expect({}).toEqual({}); // Passing
});
However, I want to test the matcher also for failing cases and I want to test the messages they produce when the matcher fails, for example in this way.
// Jasmine 2.x
it('should test if two objects are equal', function () {
expect(matcher.compare(1, 1)).toEqual({
pass: true,
message: 'Expected 1 not to equal 1' // Message is for when you use .not with the matcher
});
expect(matcher.compare(1, 2)).toEqual({
pass: false,
message: 'Expected 1 to equal 2'
});
});
The variable matcher in the code examples is the matcher function that I added using jasmine.addMatchers({ toEqual: matcher }).
Since I want not to polute global space I have no idea how to have the matcher available in my specs. The default jasmine matchers are in jasmine.matchers and they are added to their spec via some $j object.
So the main question actually is: where are my custom matchers stored when I add them to Jasmine? Can I retrieve them for the specs or are they hidden in a closure?
Edit
it seems like addMatchers is calling env.addMatchers, which pushes the matchers to runnableResources[currentRunnable().id].customMatchers. RunnableResources is a closure...

Related

Wrong Cypress intercept being hit

I have set up a number of intercepts in the body of my tests. Here they are, pasted from the Cypress log, with the alias added
cy:intercept ➟ // alias: getRecipesSku(971520)
Method: GET
Matcher: "https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&_fields**"
Mocked Response: [{ ... }]
cy:intercept ➟ // alias: getRecipesSku(971520,971310)
Method: GET
Matcher: "https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&tags[]=6289&_fields**"
Mocked Response: [{ ... }]
Our application's tests also mocks a number of routes by default, (coming from an apiClient.initialize) including this one below. FWIW, this is defined earlier than those above:
cy:intercept ➟ // alias: getJustcookRecipes
Method: GET
Matcher: "https://wpsite.com/wp-json/**"
Mocked Response: [{ ... }]
My test code sets up those first two intercepts, and ultimately calls the routes. Here is the code, heavily abridged:
it('refreshes the recipes when switching protein tabs', () => {
apiClient.initialize()
/* do lots of other stuff; load up the page, perform other tests, etc */
// call a function that sets up the intercepts. you can see from the cypress output
// that the intercepts are created correctly, so I don't feel I need to include the code here.
interceptJustCook({ skus: [beefCuts[0].id] }, [beefCut1Recipe])
interceptJustCook({ skus: [beefCuts[0].id, beefCuts[1].id] }, twoBeefRecipes)
// [#1] select 1 item;
// calls route associated with intercept "getRecipesSku(971520)"
page.click.checkboxWithSku(beefCuts[0].id)
/* assert against that call */
// [#2] select 2nd item (now 2 items are selected);
// calls route associated with intercept "getRecipesSku(971520, 971310)"
page.click.checkboxWithSku(beefCuts[1].id)
In the Cypress logs, the first call (marked by comment #1) is intercepted correctly:
cy:fetch ➟ (getRecipesSku(971520)) STUBBED
GET https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&_fields=jetpack_featured_media_url,title.rendered,excerpt.rendered,link,id&per_page=100&page=1&orderby=date
However, the second call (marked by comment #2) is intercepted by the wrong route mocker:
cy:fetch ➟ (getJustCookRecipes) STUBBED
GET https://wpsite.com/wp-json/wp/v2/posts?tags[]=6287&tags[]=6289&_fields=jetpack_featured_media_url,title.rendered,excerpt.rendered,link,id&per_page=100&page=1&orderby=date
You can see for yourself that the URL called at #2 does indeed match the getRecipesSku(971520, 971310) intercept, but it is caught by the getJustcookRecipes intercept. Now, I suppose the URL for that latter intercept would catch my second custom intercept. But it would also, in the same way, catch my first custom intercept, but that first one works.
(update:) I tried commenting out the place in the code where the getJustcookRecipes intercept is created so that it doesn't exist. Now, the call that should hit getRecipesSku(971520,971310) isn't being mocked at all! I checked and the mocked and called urls are a match.
Why is this going wrong and how do I fix it?
Something in the glob pattern for the 2nd intercept #getRecipesSku(971520,971310) is refusing to match.
It's probably not worth while analyzing what exactly (you may not be able to fix it in glob), but switching to a regex will match.
See regex101.com online test
cy.intercept(/wpsite\.com\/wp-json\/wp\/v2\/posts\?tags\[]=6287&tags\[]=6289&_fields/, {})
.as('getRecipesSku(971520,971310)')
The request query string may be badly formed
Looking at the docs for URLSearchParams, the implication is that the query string should be key/value pairs.
But the 2nd request has two identical keys using tags[] as the key.
It looks as if the correct format would be /wp/v2/posts?tags=[6287,6289] since the square brackets don't have a lot of meaning otherwise.
It may be that the server is handling the format tags[]=6287&tags[]=6289, but Cypress is probably not. If you run the following intercept you see the query object has only one tags[] key and it's the last one in the URL.
cy.intercept({
method: 'GET',
pathname: '/wp-json/wp/v2/posts',
},
(req) => {
console.log('url', req.url)
console.log('query', req.query) // Proxy {tags[]: '6289', ...
}
)
#Paolo was definitely on the right track. The WP API we're consuming uses tags[]=1,tags=[2] instead of tags[1,2] so that's correct, but some research into globs showed me that brackets are special. Why the first bracket isn't messing it up but the second one is, I'm not sure and I kind of don't care.
I thought about switching to regex, but instead I was able to escape the offender in the glob without switching to regex and needing to escape aaaallll the slashes in the url:
const interceptRecipes = (
{ category, skus }: RecipeFilterArgs,
recipes: Recipe[]
) => {
let queryString = ''
if (category) queryString = `categories[]=${CATEGORY_MAP[category]}`
if (skus) {
const tagIdMap = SkuToTagIdMap as Record<number, { tagId: number }>
// brackets break the glob pattern, so we need to escape them
queryString = skus.map((sku) => `tags\\[]=${tagIdMap[sku].tagId}`).join('&')
}
const url = `${baseUrl}?${queryString}${otherParams}`
const alias = category ? `get${category}Recipes` : `getRecipesSku(${skus})`
cy.intercept('GET', url, recipes).as(alias)
}

Does mixing javascript functions and Cypress commands contribute to test flakiness?

Among many, One of my test looks like
it("Admin is able to edit new group", () => {
cy.intercept("PUT", /\/api\/groups/).as("editGroupAPI");
cy.get("#groups").then(groups => {
const group = groups[0];
// go to edit page cancel and come back to groups page
app.groupsPage.card
.groupActionIcon(group.name, "modify")
.scrollIntoView()
.click();
app.commonElements
.toolBarTitle()
.should("have.text", "Edit Group");
app.groupsPage.groupDetailsForm.cancelButton().click();
app.commonElements.toolBarTitle().should("have.text", "Groups");
// edit group - 1
app.groupsPage.card
.groupActionIcon(group.name, "modify")
.scrollIntoView()
.click();
app.groupsPage.groupDetailsForm
.groupDescription()
.type(" edited");
app.groupsPage.groupDetailsForm.saveButton().click();
cy.wait("#editGroupAPI");
// validate that Groups page have loaded
app.commonElements.toolBarTitle().should("have.text", "Groups");
// validate whether group card description is reflected on card
app.groupsPage.card
.groupDescription(group.name)
.should("have.text", group.description + " edited");
});
});
app is top level parent obj, and this test uses Page Object Model.
One example of POM class is :
class CommonElements {
burgerMenu() {
return cy.get("#widgets-banner-appBanner-sideDrawerButton-content");
}
toolBarTitle() {
return cy.get("h1.app-toolbar__title__main-title");
}
toolBarTitleWithText(text) {
return cy.contains("h1.app-toolbar__title__main-title", text);
}
globalScopeButton() {
return cy.get("#global-scope-switch-toggleSwitch-button");
}
}
So as it is evident that, cy.wait() and then call to pageObjectModel function to grab title element:
cy.wait("#editGroupAPI");
// validate that Groups page have loaded
app.commonElements.toolBarTitle().should("have.text", "Groups");
Now sometimes this fails, so as I have seen in docs, plain js code get executed immediately, but since in this case whole test is wrapped in cy.get("alias"), will it still matter (or execute js immediately)?
This might sound very obvious, but I just want to confirm.
Final question: does mix usage of Page Object Model functions and cy.command contribute to test flakiness?
Short answer: no, mixing Cypress commands with Page Object model functions does not itself contribute to test flakiness.
Explanation: a Cypress command is never executed immediately. It does not matter if a Cypress command is called in any 'external' function (including a POM function) or directly in a test case function. Either way, Cypress commands are only enqueued when statements of a function are executed. And they later will be executed in the same order regardless whether they defined inside 'external' function or test case one.
This is also true for a command that was called inside a cypress synchronous block of code (in a then/should callback). Even in this case the command will not be executed immediately.
In a nutshell, using a POM function to call a Cypress command does not influence on how and when this command is executed and so using POM approach can not itself contribute to any test flakiness.
You can play and see the order of command execution using such a script:
You can either open Dev console to see the console output or use breakpoints to see the execution live.
The gif above shows the debugging directly from IDE (IntelliJ) using the Cypress Support Pro plugin

rspec passing instance as argument causes result with different object id

I am trying to test this example code:
SomeJob.schedule_job(title: title, body: BodyGenerator.new(recipient: user))
So the body of my rspec test is:
expect(SomeJob).to receive(:schedule_job).with(
title: 'Some title',
body: BodyGenerator.new(recipient: test_user)
).and_call_original
...
But I am getting the error after running the tests
and the only difference between expected result and actual result is the object id of the BodyGenerator
- #<BodyGenerator:0x00007fb188b50f68 ...
+ #<BodyGenerator:0x00007fb188bebe50 ...
Without and_call_original we can use be_an_instance_of(BodyGenerator)
but here it's not the case
For more context. BodyGenerator.new(recipient: user) will return specific message depending on the user type and other user's properties.
BodyGenerator.new is returning a different instance of BodyGenerator in your test than in your implementation. As such, the two do not match.
You should instead mock BodyGenerator.new such that the returned instance in your implementation matches your test:
let(:body_double) { instance_double(BodyGenerator) }
before do
allow(BodyGenerator).to receive(:new).and_return(body_double)
end
it "passes the test" do
expect(SomeJob).to receive(:schedule_job).with(
title: "Some title",
body: body_double
)
end
If necessary, you can then also set expectations against the params passed to BodyGenerator.new

Nightwatch attributeContains with multiple acceptable values

I have a successful test
browser
.url(testURL)
.waitForElementPresent('body', 1000)
.verify.attributeContains('someElement', 'someAttribute', 'foo')
But for my purposes it is acceptable for 'someAttribute' to contain 'foo' OR 'bar'. I'm wondering how I can write this kind of test so that no test failures are reported by Nightwatch.
You can test if attribute contains 'foo' OR 'bar' in two steps:
get the attribute value with getAttribute() or attribute()
match a regex against the value
With getAttribute(), use regex.test():
browser.getAttribute('someElement', 'someAttribute', function(result) {
this.assert.value(/foo|bar/.test(result.value), true);
};
With attribute(), use matches() assertion:
browser.expect.element('someElement').to.have.attribute('someAttribute')
.which.matches(/foo|bar/);
use .elements() and obtain the length of element result to avoid fail message.
.elements('css selector','someElement[yourattribute="foo"]',function(result){
if(result.value.length>0){ //element exists
console.log('somelement is here')
}
else{
console.log('not here')
}
});

Jasmine spyOn with specific arguments

Suppose I have
spyOn($cookieStore,'get').and.returnValue('abc');
This is too general for my use case. Anytime we call
$cookieStore.get('someValue') --> returns 'abc'
$cookieStore.get('anotherValue') --> returns 'abc'
I want to setup a spyOn so I get different returns based on the argument:
$cookieStore.get('someValue') --> returns 'someabc'
$cookieStore.get('anotherValue') --> returns 'anotherabc'
Any suggestions?
You can use callFake:
spyOn($cookieStore,'get').and.callFake(function(arg) {
if (arg === 'someValue'){
return 'someabc';
} else if(arg === 'anotherValue') {
return 'anotherabc';
}
});
To the ones using versions 3 and above of jasmine, you can achieve this by using a syntax similar to sinon stubs:
spyOn(componentInstance, 'myFunction')
.withArgs(myArg1).and.returnValue(myReturnObj1)
.withArgs(myArg2).and.returnValue(myReturnObj2);
details in: https://jasmine.github.io/api/edge/Spy#withArgs
One possible solution is use the expect().toHaveBeenCalledWith() to check the parameters, example:
spyOn($cookieStore,'get').and.returnValue('abc');
$cookieStore.get('someValue') --> returns 'abc';
expect($cookieStore.get).toHaveBeenCalledWith('someValue');
$cookieStore.get('anotherValue') --> returns 'abc'
expect($cookieStore.get).toHaveBeenCalledWith('anotherValue');
Another way of achieving the same result would be.... (Ideal when you are writing unit tests without using a testbed)
declare the spy in root describe block
const storageServiceSpy = jasmine.createSpyObj('StorageService',['getItem']);
and inject this spy in the place of original service in the constructor
service = new StoragePage(storageServiceSpy)
and inside the it() block...
storageServiceSpy.getItem.withArgs('data').and.callFake(() => {})

Resources