Jest - How to restructure code to make it testable? - unit-testing

I have the following function for which I'd like to add two unit tests, to cover the cases when the selector is or not on the page.
async function getPrice(page, url) {
const priceSelector = '#price';
if (await page.$(priceSelector)) {
return page.$eval(priceSelector, elem => elem.innerText);
}
return null;
}
page is defined in another function:
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url);
I've tried to mock page so that page.$(priceSelector) returns truthy or falsy but without success. The examples in the doc to mock modules make sense, but as it is, is my code testable? If not, how should it be structured?

There is only one place need to be refactored, you'd better extract the callback function elem => elem.innerText into a new function.
E.g.
index.ts:
export async function getPrice(page, url) {
const priceSelector = '#price';
if (await page.$(priceSelector)) {
return page.$eval(priceSelector, elem => elem.innerText);
}
return null;
}
index.spec.ts:
import { getPrice } from './';
const page = {
$: jest.fn(),
$eval: jest.fn()
};
beforeEach(() => {
jest.resetAllMocks();
});
test('should eval', async () => {
page.$.mockResolvedValueOnce(true);
page.$eval.mockReturnValueOnce('dummy data');
const actualValue = await getPrice(page, 'example.com');
expect(actualValue).toBe('dummy data');
expect(page.$).toBeCalledWith('#price');
expect(page.$eval).toBeCalledWith('#price', expect.any(Function));
});
test('should return null', async () => {
page.$.mockResolvedValueOnce(false);
const actualValue = await getPrice(page, 'example.com');
expect(actualValue).toBeNull();
expect(page.$).toBeCalledWith('#price');
expect(page.$eval).not.toBeCalled();
});
You can test it like this, but the callback function will not be tested and covered.
Unit test result with coverage report:
PASS src/stackoverflow/58651192/index.spec.ts
✓ should eval (6ms)
✓ should return null (2ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 85.71 | 100 | 50 | 100 | |
index.ts | 85.71 | 100 | 50 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.786s, estimated 7s
If we extract the callback function for $eval like this:
export const evalCallback = elem => elem.innerText;
We can test it easily:
test('evalCallback', () => {
const actualValue = evalCallback({ innerText: 'unit test' });
expect(actualValue).toBe('unit test');
});
Unit test result with 100% coverage:
PASS src/stackoverflow/58651192/index.spec.ts (9.066s)
✓ should eval (10ms)
✓ should return null (1ms)
✓ evalCallback (1ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 10.804s

Related

SilverStripe sapphire unit test throwing error, "Couldn't find object " when the object is retrieved from the fixture with objFromFixture method

I am working on a SilverStripe project. I am now trying to write Unit Tests and Functional Tests for my application. In my test, I am seeding the pages in the database using the fixture files. When I tried to seed the fixture for the Page class which came built-in with the SilverStripe project, it is fine. Following is my code.
class PageTest extends \SilverStripe\Dev\FunctionalTest
{
protected static $fixture_file = 'fixtures.yml';
public function testMyMethod()
{
$expectedURLs = [
'new_page' => 'testing1',
'old_page' => 'testing2',
];
foreach ($expectedURLs as $fixture => $urlSegment) {
$obj = $this->objFromFixture('Page', $fixture);
$this->assertEquals($urlSegment, $obj->URLSegment);
}
}
}
This is my fixture file.
Page:
new_page:
Title: New Page 1
Heading: Wellington
PublishedDate: 2015-12-12 00:00:01
ShowInFooterUsefulLinks: true
PageTheme: Default
URLSegment: testing1
old_page:
Title: Old Page 1
Heading: Wellington 1
PublishedDate: 2015-12-12 00:00:01
ShowInFooterUsefulLinks: true
PageTheme: Default
URLSegment: testing2
The above test is working as expected.
In my test, I wanted to use the EventPage class instead. I have a class called EventPage class that is extending from the Page class. That EventPage class table has an extra field called EventDate. So that I modified my fixture file to the one below.
EventPage:
new_page:
Title: New Page 1
Heading: Wellington
PublishedDate: 2015-12-12 00:00:01
ShowInFooterUsefulLinks: true
PageTheme: Default
URLSegment: testing1
EventDate: 2015-12-12 00:00:01
old_page:
Title: Old Page 1
Heading: Wellington 1
PublishedDate: 2015-12-12 00:00:01
ShowInFooterUsefulLinks: true
PageTheme: Default
URLSegment: testing2
EventDate: 2015-12-12 00:00:01
This time I am using EventPage class instead.
I also modified my test class to this
class PageTest extends \SilverStripe\Dev\FunctionalTest
{
protected static $fixture_file = 'fixtures.yml';
public function testMyMethod()
{
$expectedURLs = [
'new_page' => 'testing1',
'old_page' => 'testing2',
];
foreach ($expectedURLs as $fixture => $urlSegment) {
$obj = $this->objFromFixture('EventPage', $fixture);
$this->assertEquals($urlSegment, $obj->URLSegment);
}
}
}
When I run the test, I got the following error.
Fatal error: Couldn't find object 'new_page' (class: EventPage) in /var/www/vendor/silverstripe/framework/src/Dev/SapphireTest.php on line 505
Call Stack:
0.0013 349392 1. {main}() /var/www/vendor/phpunit/phpunit/phpunit:0
0.1998 520368 2. PHPUnit_TextUI_Command::main() /var/www/vendor/phpunit/phpunit/phpunit:52
0.1999 520480 3. PHPUnit_TextUI_Command->run() /var/www/vendor/phpunit/phpunit/src/TextUI/Command.php:116
0.4674 1128776 4. PHPUnit_TextUI_TestRunner->doRun() /var/www/vendor/phpunit/phpunit/src/TextUI/Command.php:186
0.5089 1210224 5. PHPUnit_Framework_TestSuite->run() /var/www/vendor/phpunit/phpunit/src/TextUI/TestRunner.php:517
0.5124 1210752 6. PHPUnit_Framework_TestSuite->run() /var/www/vendor/phpunit/phpunit/src/Framework/TestSuite.php:733
4.7228 16476656 7. PHPUnit_Framework_TestSuite->run() /var/www/vendor/phpunit/phpunit/src/Framework/TestSuite.php:733
4.7573 16484232 8. PageTest->run() /var/www/vendor/phpunit/phpunit/src/Framework/TestSuite.php:733
4.7574 16484232 9. PHPUnit_Framework_TestResult->run() /var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php:868
4.7577 16484608 10. PageTest->runBare() /var/www/vendor/phpunit/phpunit/src/Framework/TestResult.php:686
22.8349 39277864 11. PageTest->runTest() /var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php:913
22.8350 39278160 12. ReflectionMethod->invokeArgs() /var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php:1062
22.8350 39278168 13. PageTest->testMyMethod() /var/www/vendor/phpunit/phpunit/src/Framework/TestCase.php:1062
22.8350 39278168 14. PageTest->objFromFixture() /var/www/app/tests/PageTest.php:17
22.8351 39278488 15. user_error() /var/www/vendor/silverstripe/framework/src/Dev/SapphireTest.php:505
What is wrong with my test and how can I fix it?

AppSync + DynamoDB: Query with filter doesn't return all valid items

I have Appsync API connecting to a Dynamo table.
Dynamo table has data : ("id" is key and "year" is sort key)
|--------------|-------------|-------------|-------------|-------------|
| id | year | name | class | subject |
|--------------|-------------|-------------|-------------|-------------|
| 001 | 2017 | Tom | E1 | Math |
|--------------|-------------|-------------|-------------|-------------|
| 002 | 2017 | Mary | E1 | Math |
|--------------|-------------|-------------|-------------|-------------|
| 003 | 2017 | Peter | E1 | Math |
|--------------|-------------|-------------|-------------|-------------|
the schema
type Query {
listStudents(filter: TableStudentFilterInput, limit: Int, nextToken: String): StudentConnection
}
type StudentConnection {
items: [Student]
nextToken: String
}
input TableStudentFilterInput {
id: TableStringFilterInput
year: TableStringFilterInput
name: TableStringFilterInput
class: TableStringFilterInput
subject: TableStringFilterInput
}
type Student {
id: String!
year: String!
name: String
class: String
subject: String
}
Query:
query listStudentByYear {
listStudents (filter:{year:{eq:"2017"}}) {
items {
id
year
name
class
subject
}
}
}
The issue: The query return 001 and 002, but not 003.
When I tried to update "id" from 003 to 004, then the query returns correctly 001, 002, 004.
This weird issue happens quite frequently, after some times, the AppSync query returns an incomplete result (missing some).
Any suggestion is appreciated.
Check out this thread from amplify-js issues.
Essentially what is happening is a limit is applied before the filter. So if you have a limit of 20 and 003 is entry number 21 it will not be included in the filter operation.
A workaround here is to remove the limit from you resolver in the AWS AppSync Console
So change this:
#set( $ListRequest = {
"version": "2017-02-28",
"limit": $limit
})
to this:
#set( $ListRequest = {
"version": "2017-02-28",
} )
Now this isn't a graceful workaround as the DynamoDB Scan will only return 1MB of data meaning this solution will not work for large (practical) implementations.

Datalabels plugin throws error when using scriptable option

I have a doughnut chart. I'm using chartjs-plugin-datalabels. For the backgroundColor I'm using the following config:
plugins: {
datalabels: {
backgroundColor: function(context) {
return context.dataset.backgroundColor;
}
}
Based on the docs, the background color property should permit scriptable options.
I am receiving the below error:
error TS2322: Type '(context: Context) => string | string[] | CanvasGradient | CanvasPattern | ChartColor[]' is not assignable to type 'string | CanvasGradient | CanvasPattern | (string | CanvasGradient | CanvasPattern)[] | ((context: Context) => string | CanvasGradient | CanvasPattern)'.
I'm using the following libraries versions:
"chart.js": "^2.8.0",
"chartjs-plugin-datalabels": "^0.6.0",
Since the pie chart defines an array of backgroundColor for each dataset in it and the function is called for each datalabel on the chart, you will need a way to tell which color in the array the function should use, probably using the index will do the trick.
Try something like this:
return context.dataset.backgroundColor[context.dataIndex];

Using Spock test how to validate json attributes key-value pairs comes as dynamically inputs

How to write right test, in order to test the below csv data stored in database table. In the input other than item, anything could be optional.
In this, item is key, rest all goes as part of json format typically it looks like this in database {"brand": "Brand6", "category": "Category6", "subcategory": "Sub-Category6"}
Input:
item,category,subcategory,brand,type,feature
TEST-ITEM6,Category6,Sub-Category6,Brand6
TEST-ITEM7,Category7,Sub-Category7,Brand7,TYPE7,FEATURE7
TEST-ITEM8,Category8,Sub-Category8,Brand8,TYPE8,FEATURE8
Test case tried:
def "Case 3a. Verify New 2 records with two more additional fields along with earlier fields to the same tenant"() {
expect:
sql().eachRow("SELECT * FROM item WHERE item IN ('"+item+"')") { row ->
def dbItem = row[0]
def dbAttributes = getJsonToObject(row[1])
def dbCategory = dbAttributes.getAt("category").toString()
def dbSubCategory = dbAttributes.getAt("subcategory").toString()
def dbBrand = dbAttributes.getAt("brand").toString()
def dbType = dbAttributes.getAt("type").toString()
def dbFeature = dbAttributes.getAt("feature").toString()
assert dbItem == item
assert category == dbCategory
assert subcategory == dbSubCategory
assert brand == dbBrand
assert type == dbType
assert feature == dbFeature
}
where:
item << ['TEST-ITEM6', 'TEST-ITEM7', 'TEST-ITEM8']
category << ['Category6','Category7', 'Category8']
subcategory << ['Sub-Category6','Sub-Category7', 'Sub-Category8']
brand << ['Brand6','Brand7', 'Brand8']
type << ['TYPE7', 'TYPE8']
feature << ['FEATURE7', 'FEATURE8']
}
Error:
Condition not satisfied:
type == dbType
| | |
TYPE8| TYPE7
false
1 difference (80% similarity)
TYPE(8)
TYPE(7)
Expected :TYPE7
Actual :TYPE8
In this case I would recommend to use Data Tables as it becomes more readable and resembles your input more closely.
And while type and feature are optional, you need to provide some value for it. It could be null or it could be an empty List or Map (if an Item can have more than one type/feature)
So you where block might look like this:
item | category | subcategory | brand | typeFeatureMap
'TEST-ITEM6' | 'Category6' | 'Sub-Category6' | 'Brand6' | [:] // empty
'TEST-ITEM7' | 'Category7' | 'Sub-Category7' | 'Brand7' | ['TYPE7':'FEATURE7']
'TEST-ITEM8' | 'Category8' | 'Sub-Category8' | 'Brand8' | ['TYPE8':'FEATURE8']
I would also recommend to collect the data and then compare it, so you get around ordering issues.
So bofore your eachRow do something like
def itemFeatures = [:]
In your eachRow do something like
itemFeatures.put(dbAttributes.getAt("type").toString(), dbAttributes.getAt("feature").toString())
And afterwards
itemFeatures == typeFeatureMap
While not answering your question, I would recommend to think about separating the tests from your database if possible.
If you create separate tests for an database abstraction layer and your business logic, you'll be more happy in the long run ;)
For the optional fields you can use the Elvis operator ?: like this (sorry, long code, I modeled your database and two new test cases, one with many optional fields and one failing test):
package de.scrum_master.stackoverflow
import spock.lang.Specification
import spock.lang.Unroll
class DataTableWithOptionalItemsTest extends Specification {
#Unroll
def "Case 3a. Verify record '#item' with possibly optional fields"() {
expect:
testData[item].each { row ->
def dbItem = row["item"]
def dbCategory = row["category"]
def dbSubCategory = row["subcategory"]
def dbBrand = row["brand"]
def dbType = row["type"]
def dbFeature = row["feature"]
assert dbItem == item
assert (category ?: dbCategory) == dbCategory
assert (subcategory ?: dbSubCategory) == dbSubCategory
assert (brand ?: dbBrand) == dbBrand
assert (type ?: dbType) == dbType
assert (feature ?: dbFeature) == dbFeature
}
where:
item | category | subcategory | brand | type | feature
'TEST-ITEM6' | 'Category6' | 'Sub-Category6' | 'Brand6' | null | null
'TEST-ITEM7' | 'Category7' | 'Sub-Category7' | 'Brand7' | 'TYPE7' | 'FEATURE7'
'TEST-ITEM8' | 'Category8' | 'Sub-Category8' | 'Brand8' | 'TYPE8' | 'FEATURE8'
'TEST-ITEM9' | null | null | null | null | null
'TEST-FAIL' | 'CategoryX' | 'Sub-CategoryX' | 'BrandX' | 'TYPEX' | 'FEATUREX'
}
static final testData = [
'TEST-ITEM6': [
[
item : 'TEST-ITEM6',
category : 'Category6',
subcategory: 'Sub-Category6',
brand : 'Brand6',
type : 'dummy',
feature : null
],
[
item : 'TEST-ITEM6',
category : 'Category6',
subcategory: 'Sub-Category6',
brand : 'Brand6',
type : null,
feature : "foo"
]
],
'TEST-ITEM7': [
[
item : 'TEST-ITEM7',
category : 'Category7',
subcategory: 'Sub-Category7',
brand : 'Brand7',
type : 'TYPE7',
feature : 'FEATURE7'
],
[
item : 'TEST-ITEM7',
category : 'Category7',
subcategory: 'Sub-Category7',
brand : 'Brand7',
type : 'TYPE7',
feature : 'FEATURE7'
]
],
'TEST-ITEM8': [
[
item : 'TEST-ITEM8',
category : 'Category8',
subcategory: 'Sub-Category8',
brand : 'Brand8',
type : 'TYPE8',
feature : 'FEATURE8'
],
[
item : 'TEST-ITEM8',
category : 'Category8',
subcategory: 'Sub-Category8',
brand : 'Brand8',
type : 'TYPE8',
feature : 'FEATURE8'
]
],
'TEST-ITEM9': [
[
item : 'TEST-ITEM9',
category : 'Category1',
subcategory: 'Sub-Category1',
brand : 'Brand1',
type : 'TYPE1',
feature : 'FEATURE1'
],
[
item : 'TEST-ITEM9',
category : null,
subcategory: null,
brand : null,
type : null,
feature : null
]
],
'TEST-FAIL' : [
[
item : 'TEST-FAIL',
category : 'CategoryX',
subcategory: 'Sub-CategoryX',
brand : 'BrandY',
type : 'TYPEX',
feature : 'FEATUREX'
]
]
]
}

Multi Value in one line Google Graph

I want to generate in google line graph the same in the picture but I cannot to do it. I have A,B and C each of these letters have some values ( A1, A2, B1, B2 ... e.g ) The date is the same it differs only time ( minutes or seconds but day is similar ) I could generate only one point for one letter in one date:
[cols] => Array
(
[0] => Array
(
[id] =>
[label] => Timestamp
[type] => string
)
[1] => Array
(
[id] =>
[label] => A
[type] => number
)
[2] => Array
(
[id] =>
[label] => B
[type] => number
)
[3] => Array
(
[id] =>
[label] => C
[type] => number
)
)
[rows] => Array
(
[0] => Array
(
[c] => Array
(
[0] => Array
(
[v] => 2014-01-30
)
[1] => Array
(
[v] => 120
)
[2] => Array
(
[v] => 100
)
[3] => Array
(
[v] => 35
)
)
)
[1] => Array
(
[c] => Array
(
[0] => Array
(
[v] => 2014-01-30
)
[1] => Array
(
[v] => 334
)
[2] => Array
(
[v] => 55
)
[3] => Array
(
[v] => 15
)
)
)
)
These code gives me 3 seperate lines with only 3 values in one date ( 2014-01-30 ) and next date is also the same ( 2014-01-30 ) But I want to collect all these data in one line as I mentioned in photo below. Thanks in advance for everybody!
Making this work is going to require a bit of trickery. You need to organize your data like this:
Date | Type | Value | Label
---------------------------------
30.01.2014 | A | 75 | A1
30.01.2014 | A | 100 | A2
30.01.2014 | A | 125 | A3
30.01.2014 | B | 150 | B1
30.01.2014 | B | 175 | B2
30.01.2014 | B | 200 | B3
30.01.2014 | C | 180 | C1
30.01.2014 | C | 210 | C2
30.01.2014 | C | 270 | C3
31.01.2014 | A | 75 | A1
31.01.2014 | A | 100 | A2
31.01.2014 | A | 125 | A3
31.01.2014 | B | 150 | B1
31.01.2014 | B | 175 | B2
31.01.2014 | B | 200 | B3
31.01.2014 | C | 180 | C1
31.01.2014 | C | 210 | C2
31.01.2014 | C | 270 | C3
The data needs to be ordered in the order you want the line drawn (so if you want the line to be in the order A1, A2, A3, B1, B2, B3 on 30.01.2014, then that is the order it must be in the table).
Next, you need to use a DataView to split this into multiple series of data to get the points color-coded like your legend:
var view = new google.visualization.DataView(data);
view.setColumns([0, {
type: 'number',
label: 'A',
calc: function (dt, row) {
return (dt.getValue(row, 1) == 'A') ? dt.getValue(row, 2) : null;
}
}, {
type: 'string',
role: 'annotation',
calc: function (dt, row) {
return (dt.getValue(row, 1) == 'A') ? dt.getValue(row, 3) : null;
}
}, {
type: 'number',
label: 'A',
calc: function (dt, row) {
return (dt.getValue(row, 1) == 'B') ? dt.getValue(row, 2) : null;
}
}, {
type: 'string',
role: 'annotation',
calc: function (dt, row) {
return (dt.getValue(row, 1) == 'B') ? dt.getValue(row, 3) : null;
}
}, {
type: 'number',
label: 'A',
calc: function (dt, row) {
return (dt.getValue(row, 1) == 'C') ? dt.getValue(row, 2) : null;
}
}, {
type: 'string',
role: 'annotation',
calc: function (dt, row) {
return (dt.getValue(row, 1) == 'C') ? dt.getValue(row, 3) : null;
}
}, 2]);
Then, when drawing the chart, set the series option to make the points and line appear correctly:
series: {
0: {
// series A options
pointSize: 3,
lineWidth: 0
// you can set the color here with the "color" option if you want
},
1: {
// series B options
pointSize: 3,
lineWidth: 0
// you can set the color here with the "color" option if you want
},
2: {
// series C options
pointSize: 3,
lineWidth: 0
// you can set the color here with the "color" option if you want
},
3: {
// this series draws the line
pointSize: 0,
lineWidth: 1,
visibleInLegend: false,
enableInteractivity: false
// you can set the color here with the "color" option if you want
}
}
See an example here: http://jsfiddle.net/asgallant/bn9tE/