Angular Testing: Difference between revisions
No edit summary |
|||
Line 4: | Line 4: | ||
describe('CalculatorService', ()=> { | describe('CalculatorService', ()=> { | ||
it('Should add two numbers', () => { | |||
it('Should add two numbers pending', () => { | |||
pending() | pending() | ||
}); | }); | ||
it('Should subtract two numbers', () => { | it('Should subtract two numbers fail', () => { | ||
fail() | fail() | ||
}); | |||
it('Should add two numbers', () => { | |||
const calculator = new CalculatorService(new LoggerService()) | |||
const result = calculator.add(2,2) | |||
expect(result).toBe(4) | |||
}); | }); | ||
}) | }) | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=Setting up Karma= | =Setting up Karma= | ||
This took a lifetime to set up and make me wonder if I am using the right tool. It comes with Angular to stayed with it. What took the time was setting up the console.log to work. Here is my karma.conf.js. | This took a lifetime to set up and make me wonder if I am using the right tool. It comes with Angular to stayed with it. What took the time was setting up the console.log to work. Here is my karma.conf.js. |
Revision as of 04:53, 14 December 2021
Jasmine
Angular by default using Jasmine for testing. Test follow the same approach as other frameworks. You can use pending and fail to simulate pending and guess what, failing.
describe('CalculatorService', ()=> {
it('Should add two numbers pending', () => {
pending()
});
it('Should subtract two numbers fail', () => {
fail()
});
it('Should add two numbers', () => {
const calculator = new CalculatorService(new LoggerService())
const result = calculator.add(2,2)
expect(result).toBe(4)
});
})
Setting up Karma
This took a lifetime to set up and make me wonder if I am using the right tool. It comes with Angular to stayed with it. What took the time was setting up the console.log to work. Here is my karma.conf.js.
module.exports = function (config) {
config.set({
basePath: "",
frameworks: ["jasmine", "@angular-devkit/build-angular"],
plugins: [
require("karma-jasmine"),
require("karma-chrome-launcher"),
require("karma-jasmine-html-reporter"),
require("karma-coverage-istanbul-reporter"),
require("karma-coverage"),
require("@angular-devkit/build-angular/plugins/karma"),
],
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
browserConsoleLogOptions: {
terminal: true,
level: ""
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageIstanbulReporter: {
dir: require("path").join(__dirname, "./coverage/debug-tests"),
reports: ["html", "lcovonly", "text-summary"],
fixWebpackSourcePaths: true,
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/bibble-park-app'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ["progress", "kjhtml"],
port: 9876,
colors: true,
logLevel: config.LOG_DEBUG,
autoWatch: true,
browsers: ["Chrome"],
singleRun: false,
restartOnFileChange: true,
});
};
RxJs Testing
Decided to look a bit more into how to test observables. My starting point is Kevin Kreuzer's Medium page here.
The RxJs team provides and approach where you define a scheduler and a RunHelper
interface RunHelpers {
cold: typeof TestScheduler.prototype.createColdObservable
hot: typeof TestScheduler.prototype.createHotObservable
flush: typeof TestScheduler.prototype.flush
time: typeof TestScheduler.prototype.createTime
expectObservable: typeof TestScheduler.prototype.expectObservable
expectSubscriptions: typeof TestScheduler.prototype.expectSubscriptions
animate: (marbles: string) => void
}
Let's define the code we are testing which is function which filters out odd numbers and multiplies the even ones by 10.
const evenTimesTen = (source$: Observable<number>): Observable<number> => {
return source$.pipe(
filter((n) => n % 2 === 0),
map((n) => n * 10),
)
}
Simple Test
Then a define the test case like any Jasmine case.
describe('ExampleRxjsTest', () => {
let scheduler: TestScheduler
beforeEach(() => {
scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected)
})
})
it('should filter out odd numbers', () => {
scheduler.run((runhelpers: RunHelpers) => {...})
})
})
The author suggested that destructuring the interface implementation makes it a log easier to read. In this example we will define code and expectObservable.
/*
it('should filter out odd numbers', () => {
scheduler.run((runhelpers: RunHelpers) => {...})
})
*/
it('should filter out odd numbers', () => {
scheduler.run(({ cold, expectObservable }) => {
const values = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }
const source$ = cold('a-b-c-d-e-f|', values)
const expectedMarble = '--a---b---c|'
const expectedValues = { a: 20, b: 40, c: 60 }
const result$ = evenTimesTen(source$)
expectObservable(result$).toBe(expectedMarble, expectedValues)
})
})
Testing with an Error
I like this because I found my trusted divide by zero does not work with TS/Javascript. Much the same as above but this time we are testing for an error. Here is the function
const tenDivideByValue = (
source$: Observable<number>,
): Observable<number> | Observable<unknown> => {
return source$.pipe(
map((n) => {
const result = 10 / n
console.log(`Value we got was ${result}`)
if (!isFinite(result)) {
throw new Error('Divide by zero')
} else {
return result
}
}),
)
}
And here is the test case. The tricky part was understanding errorvalue meant an instance of Error.
it('should fail on divide by zero', () => {
const errorValue = new Error('Divide by zero')
scheduler.run(({ cold, animate, expectObservable }) => {
const values = { a: 5, b: 2, c: 10, d: 0 }
const source$ = cold('--a---b---c---d---|', values)
const expectedMarble = '--a---b---c---#'
animate(' ----r----r---r--')
const expectedValues = { a: 2, b: 5, c: 1 }
const result$ = tenDivideByValue(source$)
expectObservable(result$).toBe(expectedMarble, expectedValues, errorValue)
})
})
Testing with an catchError
Again a long struggle to get this working but here we go.
const tenDivideByValueCatchError = (
source$: Observable<number>,
): Observable<number> | Observable<unknown> => {
return source$.pipe(
map((n) => {
const result = 10 / n
console.log(`Value we got was ${result}`)
if (!isFinite(result)) {
throw new Error('Divide by zero')
} else {
return result
}
}),
catchError((error) => {
return of({})
}),
)
}
And here is the test case. The tricky part this time was understanding how to define on frame 4 emit a and complete.
it('should fail on divide by zero with catch', () => {
scheduler.run(({ cold, expectObservable }) => {
const values = { a: 5, b: 2, c: 10, d: 0 }
const source$ = cold('--a---b---c---d|', values)
const expectedMarble = '--a---b---c---(d|)'
const expectedValues = { a: 2, b: 5, c: 1, d: {} }
const result$ = tenDivideByValueCatchError(source$)
expectObservable(result$).toBe(expectedMarble, expectedValues)
})
})
Testing with an concat with cold
Struggled again to work off the examples on the net but getting through it has allowed me to understand the concepts.
const concatHot = (
source1$: Observable<number>,
source2$: Observable<number>,
): Observable<number> => {
return source1$.pipe(concat(source2$))
}
And the test case
it('should work with cold observables', () => {
scheduler.run(({ cold, expectObservable }) => {
const values1 = { a: 5, b: 2 }
const source1$ = cold('-a---b-|', values1)
const values2 = { c: 10, d: 0 }
const source2$ = cold('-c---d-|', values2)
const expectedMarbles = '-a---b--c---d-|'
const expectedValues = { a: 5, b: 2, c: 10, d: 0 }
const result$ = concatCold(source1$, source2$)
expectObservable(result$).toBe(expectedMarbles, expectedValues)
})
})
Testing with an concat with hot
I don't quite understand how we set the test up in real life. My guess it that the hot observable is mocking the timings of the release of the results.
const concatHot = (
source1$: Observable<number>,
source2$: Observable<number>,
): Observable<number> => {
return source1$.pipe(concat(source2$))
}
Here is the test case which does make sense.
/*
When testing hot observables you can specify the subscription
point using a caret ^, similar to how you specify subscriptions
when utilizing the expectSubscriptions assertion.
*/
it('should work with hot observables', () => {
scheduler.run(({ hot, expectObservable }) => {
const values1 = { a: 5, b: 2 }
const source1$ = hot('---a--^-b-|', values1)
const values2 = { c: 10, d: 0 }
const source2$ = hot('-----c^----d-|', values2)
const expectedMarbles = '--b--d-|'
const expectedValues = { b: 2, d: 0 }
const result$ = concatHot(source1$, source2$)
expectObservable(result$).toBe(expectedMarbles, expectedValues)
})
})
Marble Testing Anotation
You will see the source$ and expectedMarble have characters in them. This is a language that the library supports. Below is a list of some of these which can be found in detail at [1]
- '-' or '------': Equivalent to NEVER, or an observable that never emits or errors or completes.
- |: Equivalent to EMPTY, or an observable that never emits and completes immediately.
- #: Equivalent to throwError, or an observable that never emits and errors immediately.
- '--a--': An observable that waits 2 "frames", emits value a on frame 2 and then never completes.
- '--a--b--|': On frame 2 emit a, on frame 5 emit b, and on frame 8, complete.
- '--a--b--#': On frame 2 emit a, on frame 5 emit b, and on frame 8, error.
- '-a-^-b--|': In a hot observable, on frame -2 emit a, then on frame 2 emit b, and on frame 5, complete.
- '--(abc)-|': on frame 2 emit a, b, and c, then on frame 8, complete.
- '-----(a|)': on frame 5 emit a and complete.
- 'a 9ms b 9s c|': on frame 0 emit a, on frame 10 emit b, on frame 9,011 emit c, then on frame 9,012 complete.
- '--a 2.5m b': on frame 2 emit a, on frame 150,003 emit b and never complete.