I am still trying to get in my head the object fundamentals in javascript which seems to be quite different than classical paradigm. I have written a toy example to fetch weather data, the code is below:
import axios from 'axios'
const weatherService = {
fetch: async endpoint => await axios.get(endpoint)
}
const weatherApi = {
currentWeather: async city => {
try {
const { data: { data } } = await this.service.fetch(this.config.endpoints.curr)
return this.handleCurrentData(data)
} catch(e) {
this.handleError(e)
}
},
hourlyForecast: async city => {
try {
const { data: { data } } = await this.service.fetch(this.config.endpoints.hour)
return this.handleHourlyData(data)
} catch(e) {
this.handleError(e)
}
}
};
const defaultConfig = {
key: process.env.API_KEY,
endpoints: {
curr: `/current/geosearch?key=${this.key}`,
hour: `/forecast/3hourly/geosearch?key=${this.key}`
}
};
const api = (api, config, service) => {
return {
Object.create(weatherApi),
Object.create(service),
Object.create(config),
...obj
}
};
// dependency injection for testing
module.exports = (obj, config=defaultConfig, service=weatherService) => {
return api(obj, config, service)
};
// Usage
const weatherAPI = require('api')({
handleCurrentData: (data) => console.log(data),
handleHourlyData: (data) => console.log(data)
});
weatherAPI.currentWeather('London');
weatherAPI.hourlyWeather('London');
I would like to know if I am going in the correct direction? If not what are improvement in thinking process as well as in code needed?
PS: I know I could have written the above api easily by exporting functions but this is an attempt to learn object composition.
Related
I am not getting any clue how to mock a method. I have to write a unit test for this function:
index.ts
export async function getTenantExemptionNotes(platform: string) {
return Promise.all([(await getCosmosDbInstance()).getNotes(platform)])
.then(([notes]) => {
return notes;
})
.catch((error) => {
return Promise.reject(error);
});
}
api/CosmosDBAccess.ts
import { Container, CosmosClient, SqlQuerySpec } from "#azure/cosmos";
import { cosmosdbConfig } from "config/Config";
import { Workload } from "config/PlatformConfig";
import { fetchSecret } from "./FetchSecrets";
export class CosmoDbAccess {
private static instance: CosmoDbAccess;
private container: Container;
private constructor(client: CosmosClient) {
this.container = client
.database(cosmosdbConfig.database)
.container(cosmosdbConfig.container);
}
static async getInstance() {
if (!CosmoDbAccess.instance) {
try {
const connectionString = await fetchSecret(
"CosmosDbConnectionString"
);
const client: CosmosClient = new CosmosClient(connectionString);
// Deleting to avoid error: Refused to set unsafe header "user-agent"
delete client["clientContext"].globalEndpointManager.options
.defaultHeaders["User-Agent"];
CosmoDbAccess.instance = new CosmoDbAccess(client);
return CosmoDbAccess.instance;
} catch (error) {
// todo - send to app insights
}
}
return CosmoDbAccess.instance;
}
public async getAllNotesForLastSixMonths() {
const querySpec: SqlQuerySpec = {
// Getting data from past 6 months
query: `SELECT * FROM c
WHERE (udf.convertToDate(c["Date"]) > DateTimeAdd("MM", -6, GetCurrentDateTime()))
AND c.IsArchived != true
ORDER BY c.Date DESC`,
parameters: [],
};
const query = this.container.items.query(querySpec);
const response = await query.fetchAll();
return response.resources;
}
}
export const getCosmosDbInstance = async () => {
const cosmosdb = await CosmoDbAccess.getInstance();
return cosmosdb;
};
index.test.ts
describe("getExemptionNotes()", () => {
beforeEach(() => {
jest.resetAllMocks();
});
it("makes a network call to getKustoResponse which posts to axios and returns what axios returns", async () => {
const mockNotes = [
{
},
];
const cosmosDBInstance = jest
.spyOn(CosmoDbAccess, "getInstance")
.mockReturnValue(Promise.resolve(CosmoDbAccess.instance));
const kustoResponseSpy = jest
.spyOn(CosmoDbAccess.prototype, "getAllNotesForLastSixMonths")
.mockReturnValue(Promise.resolve([mockNotes]));
const actual = await getExemptionNotes();
expect(kustoResponseSpy).toHaveBeenCalledTimes(1);
expect(actual).toEqual(mockNotes);
});
});
I am not able to get instance of CosmosDB or spyOn just the getAllNotesForLastSixMonths method. Please help me code it or give hints. The complexity is because the class is singleton or the methods are static and private
I'm mocking the #elastic/elasticsearch library and I want to test that the search method is called with the right arguments but I'm having issues accessing search from my tests.
In my ES mock I just export an object that includes a Client prop that returns another object that has the search prop. This is the way search is accessed from the library
const { Client } = require('#elastic/elasticsearch')
const client = new Client(...)
client.search(...)
__mocks__/#elastic/elasticsearch
module.exports = {
Client: jest.fn().mockImplementation(() => {
return {
search: (obj, cb) => {
return cb(
'',
{
statusCode: 200,
body: {
hits: {
hits: [
{
_source: esIndexes[obj.index]
}
]
}
}
}
)
}
}
})
}
__tests__/getAddresses.test.js
const { getAddresses } = require('../src/multiAddressLookup/utils/getAddresses')
const { Client } = require('#elastic/elasticsearch')
beforeEach(() => {
process.env.ES_CLUSTER_INDEX = 'foo'
process.env.ui = '*'
})
describe('multiAddressLookup', () => {
test('Should return the correct premises data with only the relevant "forecasted_outages"', async () => {
const event = {
foo: 'bar'
}
const esQueryResponse = {
"body": "\"foo\":\"bar\"",
"headers": {"Access-Control-Allow-Origin": '*'},
"statusCode": 200
}
await expect(getAddresses(event)).resolves.toEqual(esQueryResponse)
expect(Client().search).toHaveBeenCalled() // This fails with 0 calls registered
})
})
I'm not sure of any exact documentation for this scenario but I got the idea while looking through the Jest: The 4 ways to create an ES6 Mock Class - Automatic mock portion of the Jest documentation.
First, the search method in the ES mock, __mocks__/#elastic/elasticsearch, needs to be converted into a jest mock function, jest.fn(). Doing this gives us access to properties and values that jest mocks provide.
__mocks__/#elastic/elasticsearch.js converted
module.exports = {
Client: jest.fn().mockImplementation(() => {
return {
search: jest.fn((obj, cb) => {
return cb(
'',
{
statusCode: 200,
body: {
hits: {
hits: [
{
_source: esIndexes[obj.index]
}
]
}
}
}
)
})
}
})
}
Second, in our tests we need to follow the path from the Client mock class until we find out methods. The syntax is MockClass.mock.results[0].value.mockFunction.
Example Test
const { Client } = require('#elastic/elasticsearch') // This is located in the "__mocks__" folder in the root of your project
const { getAddresses } = require('../../src/getAddresses') // This is the file we wrote and what we are unit testing
describe('getAddresses', () => {
it('Should call the ES Search method', async () => {
const event = { ... }
const expected = { ... }
await expect(getAddresses(event)).resolves.toEqual(expected) // pass
expect(Client.mock.results[0].value.search).toHaveBeenCalled() // pass
})
})
A JavaScript app uses Web Audio API to create sounds from JSON data. I am fetching weather data, going through the JSON data and setting their properties to variables then using those variables to manipulate my application and create sounds. Each function in it's own JavaScript module script file. The main.js not shown here is the entry point to app.
A sample JSON that will get replaced with real weather data.
dummy-data.json
{
"weather": {
"temp": 4,
"rain": 1,
"wind": 1.2
}
}
The fetch API logic.
fetchWeather.js
import { manageData} from './manageScript.js';
const DUMMY = '../dummy-data.json';
const fetchWeather = () => {
fetch(DUMMY)
.then((res) => {
return res.json();
})
.then((data) => {
manageData(data); // attaches JSON weather properties to variables
})
.catch((error) => {
console.log(error);
});
};
export { fetchWeather };
Attaches the JSON data to variables.
manageScript.js
function manageData(data) {
let rain = data.weather.rain;
//let wind = data.weather.wind;
let rainProbability;
if (rain == 1) {
rainProbability = 1;
}
else {
rainProbability = 0;
}
return rainProbability; // not sure how to return the data....?
};
export { manageData };
I want the variables from manageData function above to work here
makeSynth.js
import { manageData } from './manageScript.js';
const createSynth = () => {
//Web Audio API stuff goes here to create sounds from the variables.
//How do I get the variables to work here. Code below does not work!
let soundOfRain = manageData().rainProbability;
console.log(soundOfRain);
};
You can achieve this by refactoring your promises into a async/await pattern then returning the result (a different method of dealing with promises). Also - your createSynth function should be calling fetchWeather, not manageScript
dummy-data.json
{
"weather": {
"temp": 4,
"rain": 1,
"wind": 1.2
}
}
manageScript.js
function manageData(data) {
let rain = data.weather.rain;
//let wind = data.weather.wind;
let rainProbability;
if (rain == 1) {
rainProbability = 1;
} else {
rainProbability = 0;
}
return rainProbability;
}
export { manageData };
fetchWeather.js
import { manageData } from "./manageScript.js";
const DUMMY = "../dummy-data.json";
// Use async/await to be able to return a variable out from the promise
const fetchWeather = async () => {
const raw = await fetch(DUMMY);
const json_data = await raw.json();
const rain = manageData(json_data);
// Now you should be able to return the variable back out of the function
return rain;
};
export { fetchWeather };
makeSynth.js
import { fetchWeather } from "./fetchWeather.js";
const createSynth = async () => {
//Web Audio API stuff goes here to create sounds from the variables.
//Need to call fetchWeather (which in turn will call manageData)
let soundOfRain = await fetchWeather();
console.log(soundOfRain);
};
createSynth();
// dummy-data.json
{
"weather": {
"temp": 4,
"rain": 1,
"wind": 1.2
}
}
// fetchWeather.js
import { getRainProbability } from './get-rain-probability.js'
import { createSynth } from './create-synth.js'
const DUMMY = '../dummy-data.json'
const fetchWeather = () => {
return fetch(DUMMY)
.then((res) => res.json())
.then((data) => {
createSynth({ rainProbability: getRainProbability(data) })
})
.catch((error) => {
console.log(error)
});
};
export { fetchWeather }
// get-rain-probability.js
function getRainProbability(data) {
let rain = data.weather.rain
let rainProbability;
if (rain == 1) {
rainProbability = 1;
}
else {
rainProbability = 0;
}
return rainProbability; // not sure how to return the data....?
};
// create-synth.js
const createSynth = ({ rainProbability }) => {
const soundOfRain = //WebAPI stuff for audio using `rainProbability`
console.log(soundOfRain);
};
export { createSynth }
You can add data as a property of manageData that would return this, and access it with manageData().data; :
fetchWeather.js
const fetchWeather = () => {
fetch(DUMMY)
.then(res => {
return res.json();
})
.then(data => {
manageData.data = data; // attaches JSON weather properties to variables
})
.catch(error => {
console.log(error);
});
};
manageScript.js
function manageData() {
// ...
return this;
}
makeSynth.js
let soundOfRain = manageData().data.rainProbability;
im having this method in my component that makes an API call with axios, I checked the docs on how to test it but I cant seem to figure out how to do so. Any help would be appreciated.
loadContents() {
axios.get('/vue_api/content/' + this.slug).then(response => {
this.page_data = response.data.merchandising_page
}).catch(error => {
console.log(error)
})
},
You could use moxios or axios-mock-adapter to automatically mock Axios requests. I prefer the latter for developer ergonomics.
Consider this UserList component that uses Axios to fetch user data:
// UserList.vue
export default {
data() {
return {
users: []
};
},
methods: {
async loadUsers() {
const { data } = await axios.get("https://api/users");
this.users = data;
}
}
};
With axios-mock-adapter, the related test stubs the Axios GET requests to the API URL, returning mock data instead:
import axios from "axios";
const MockAdapter = require("axios-mock-adapter");
const mock = new MockAdapter(axios);
import { shallowMount } from "#vue/test-utils";
import UserList from "#/components/UserList";
describe("UserList", () => {
afterAll(() => mock.restore());
beforeEach(() => mock.reset());
it("loads users", async () => {
mock
.onGet("https://api/users")
.reply(200, [{ name: "foo" }, { name: "bar" }, { name: "baz" }]);
const wrapper = shallowMount(UserList);
await wrapper.vm.loadUsers();
const listItems = wrapper.findAll("li");
expect(listItems).toHaveLength(3);
});
});
demo
I'm implementing service between a view and a Rest API.
Beside, i'm completly new to stamp programming and i'm in search of some advices about this sort of code:
import {compose, methods} from '#stamp/it'
import ArgOverProp from '#stamp/arg-over-prop'
import {template} from 'lodash'
const createLogger = name => (...args) => console.log('['+ name + ']', ...args)
const HasHttpClient = ArgOverProp.argOverProp('httpClient')
const HasApiVersion = ArgOverProp.argOverProp('apiVersion')
const HasUrl = ArgOverProp.argOverProp('url')
const UseRestApi = compose(HasHttpClient, HasApiVersion, HasUrl).init([
function () {
this.getUrl = template(this.url)
this.useRestApiLog = createLogger('UseRestApi')
}
]).methods({
query: function query(method, {params, headers, body}) {
const {apiVersion} = this
const q = {
baseURL: this.getUrl({apiVersion}),
method,
...params != null && {params},
...headers != null && {headers},
...body != null && {body}
}
this.useRestApiLog('request config:', q)
return q
}
})
const WithGetOperation = compose(UseRestApi).init([
function () {
this.withGetOperationLog = createLogger('WithGetOperation')
}
]).methods({
'get': function get ({params}) {
const q = this.query('get', {headers: {'Accept': 'application/json'}, params})
this.withGetOperationLog('getting data')
return this.httpClient(q)
}
})
const CustomerRestApi = compose(WithGetOperation).init([
function () {
this.customerRestApiLog = createLogger('CustomerRestApi')
}
]).methods({
all: function all() {
this.customerRestApiLog('get all customers')
return this.get({params: {page: 1, limit: 15}})
}
})
const customerProvider = CustomerRestApi({
url: 'http://sample.com/<%=apiVersion%>/customers',
apiVersion: 'v1',
httpClient: function(config) {
return Promise.resolve({
status: 200,
config
})
}
})
const appLog = createLogger('APP')
customerProvider.all()
.then(r => appLog('HTTP response code:', r.status))
Am i in the right directions?
Especially, the createLogger thing seems ugly!
How to inject a prefixed logger into each stamp ?
How to extend that to warn, error, ... methods ?
Your logger looks just fine. 👍 It is not necessary to create every bit as a stamp. However, if you want to make the logger as a reusable stamp then you can do the same way as ArgOverProp is implemented.
Ruffly ArgOverProp is done this way:
const ArgOverProp = stampit.statics({
argOverProp(...args) {
return this.deepConf({ArgOverProp: [...args]});
}
})
.init(function (options, {stamp}) {
const {ArgOverProp} = stamp.compose.deepConfiguration;
for (let assignableArgName of ArgOverProp) {
this[assignableArgName] = options[assignableArgName];
}
});
Your logger could look like this (not necessary exactly like this):
import {argOverProp} from '#stamp/arg-over-prop';
const Logger = stampit(
argOverProp('prefix'),
{
methods: {
log(...args){ console.log(this.prefix, ...args); },
error(...args){ console.error(this.prefix, ...args); },
warn(...args){ console.warn(this.prefix, ...args); }
}
}
);
const HasLogger = stampit.statics({
hasLogger(name) {
return this.conf({HasLogger: {name}});
}
})
.init(_, {stamp}) {
const {HasLogger} = stamp.compose.configuration;
if (HasLogger) {
this.logger = Logger({prefix: HasLogger.name});
}
});
And usage:
const CustomerRestApi = stampit(
WithGetOperation,
HasLogger.hasLogger('CustomerRestApi'),
{
methods: {
all() {
this.logger.log('get all customers');
return this.get({params: {page: 1, limit: 15}});
}
}
);
I always prefer readability. So, the code above, I hope, is readable to you and any stampit newbie.
PS: a tip. The stampit and the stampit.compose you imported above are the same exact function. :) See source code.