A test to check that a schema defined validation requirement catches an invalid document on insert always fails with a message indicating a validation failure. The test is intended to pass if the invalid document is caught.
What is the appropriate way to construct this test?
Have considered venturing into package testing of Collection2 but am really not interested here in proving that package works. Rather I want to verify my schema is correctly built to pass the project requirements.
Context:
Windows 7
meteor#1.1.6
aldeed:autoform#5.4.0
aldeed:collection2#2.3.3
aldeed:simple-schema#1.3.3
velocity:core#0.9.3
sanjo:jasmine#0.16.4
Requirements:
1. Pulmonary Function test results (PFTs) are stored.
2. A pft document must contain a date (pftDate) and a Subject Id (subjId).
Schema:
PFTs = new Meteor.Collection('pfts');
Schema = {};
Schema.PFTs = new SimpleSchema({
subjId: {
type: String,
autoform: {
type: "hidden",
label: false,
},
},
pftDate: {
type: Date,
label: 'Date',
max: function(){ return new Date() },
},
});
PFTs.attachSchema(Schema.PFTs);
Server Integration Tests:
"use strict";
describe("PFTs", function(){
it("must be created with both subjId and pftDate set", function(){
var testDate = new Date();
var validNewPFT = {pftDate: testDate, subjId: '1'}
var invalidNewPFT = {};
// Fails.
// No std Jasmine matcher seems to recognize that
// the validation has caught the invalid document.
expect( PFTs.insert(invalidNewPFT) ).toThrow();
// Passes.
expect( PFTs.insert(validNewPFT) ).notToThrow();
});
});
The Velocity test result:
Error: Subj is required
packages/aldeed:collection2/collection2.js:369:1: Error: Subj is required
at getErrorObject (packages/aldeed:collection2/collection2.js:369:1)
at [object Object].doValidate (packages/aldeed:collection2/collection2.js:352:1)
at [object Object].Mongo.Collection. (anonymous function) [as insert] (packages/aldeed:collection2/collection2.js:154:1)
at app\tests\jasmine\server\integration\pftDataModelSpec.js:8:18
Discussion under an issue on GitHub yielded the following solution:
"use strict";
describe("The PFT Schema", function(){
it("contains keys for subjId and pftDate", function(){
var schemaKeys = PFTs._c2._simpleSchema._firstLevelSchemaKeys;
expect(schemaKeys).toContain('subjId');
expect(schemaKeys).toContain('pftDate');
});
describe("context", function(){
var ssPFTContext = Schema.PFTs.namedContext("pft");
it("requires the presence of subjId & pftDate", function(){
var validPFTData = {subjId: 1, pftDate: new Date()};
expect( ssPFTContext.validate(validPFTData) ).toBeTrue;
});
it("fails if subjId is absent", function(){
var invalidPFTData = {pftDate: new Date()};
expect( ssPFTContext.validate(invalidPFTData) ).toBeFalse;
});
it("fails if pftDate is absent", function(){
var invalidPFTData = {subjId: 1};
expect( ssPFTContext.validate(invalidPFTData) ).toBeFalse;
});
});
});
You must pass a function to expect that you expect to throw:
expect(function () { PFTs.insert(invalidNewPFT); }).toThrow();
expect(function () { PFTs.insert(validNewPFT); }).not.toThrow();
As you can see it is .not.toThrow() instead of notToThrow().
#Sanjo Thanks for the guidance.
The following solves the question posed:
"use strict";
describe("PFTs", function(){
it("cannot be created without a subjId", function(){
var testDate = new Date();
var invalidNewPFT = {pftDate: testDate}
var preinsertCount = PFTs.find().fetch().length;
// expect used here to swallow the msg sent to the browser by Collection2
expect( function(){ PFTs.insert(invalidNewPFT); }).toThrow;
// Verify that a record was not saved
expect( PFTs.find().fetch().length ).toEqual(preinsertCount);
});
});
The first expect swallows the message sent to the browser by Collection2. Interestingly it does not matter whether .toThrow or .not.toThrow are used, the effect is the same. The real test is the check for an increase in the number of PFTs documents by one.
Related
I'm doing an aggregation in Meteor where I'm trying to find 'thingies' within a given distance and publish it to the client:
Meteor.publish("thingieSearch", function(userId) {
check(userId, String);
var subscription = this;
var thingies = {};
var userId = this.userId;
var usrAcc = Meteor.users.findOne({_id: userId});
var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
var pipeline = [{
$geoNear: {
near: usrAcc.profile.location.geometry.coordinates,
distanceField: "calculatedDistance",
spherical: true,
limit: 100,
distanceMultiplier: 3959.2,
maxDistance: 0.075,
query: {
"status": "started",
"owner": {$ne: userId} },
}
}];
db.collection("thingies").aggregate(
pipeline,
Meteor.bindEnvironment(
function (err, result) {
console.log('result', result);
_.each(result, function (r) {
chases[r._id] = r;
subscription.added("thingieSearch", r._id, {
chase: r
});
})
}
)
);
subscription.ready();
});
When I do a console.log on the server side, it looks correct, the 'distanceField' that I specified as 'calculatedDistance' is calculated and shown as a field.
On the client side, I subscribe to this publication and I can see the thingies but I can't see the 'calculatedDistance' field.
Any idea why?
There are two things here.
1) Your publication will not reactively update data to the client, since you are using mongodb remote collection driver. If you intend it to be a non-reactive then you can use a meteor method instead of publication and call the method whenever userId changes.
2) I think you are using thingies collection on the client side and not seeing the calculatedDistance field. You need to create a client only collection (thingieSearch) to access the custom published results like this,
//On client side only
thingieSearch = new Mongo.Collection("thingieSearch");
thingieSearch.findOne(); // After the publication, you should be able to see the results with calculatedDistance
You should use whatever name you passed inside the subscription.added block to create collection. For example, if your publication has
subscription.added("thingieWithCalculateField", r._id, { chase: r });
you should do
//On client side only
thingieWithCalculateField = new Mongo.Collection("thingieWithCalculateField");
// instead of thingieSearch = new Mongo.Collection("thingieSearch");
See the counts-by-room publication in the Meteor.publish documentation for more details.
I'm defining a route that will show an appointment for a patient. I would like the template to show both the patient information and the appointment information.
I have this published:
Meteor.publish('userAppointment', function(appointmentId){
check(appointmentId, String);
var userId = Appointments.findOne(appointmentId).patientId;
return [
Appointments.find({_id: appointmentId}),
Meteor.users.find({_id: userId}, {fields: {profile: true, emails: true}})
];
});
Unfortunately Iron Router doesn't seem to be successfully waiting on the data subscription to complete before it tries to set the data context.
Note where I put debugger:
Router.route('/admin/appointment/:id', {
name: 'AppointmentShow',
waitOn: function(){
return [
Meteor.subscribe("userAppointment", this.params.id)
]
},
data: function(){
var appointmentId = this.params.id;
debugger
var patientId = Appointments.findOne(appointmentId).patientId;
return {
appointment: Appointments.findOne(appointmentId),
patient: Meteor.users.findOne(patientId)
}
}
});
At the time when debugger stops the code, when I do Meteor.users.find().fetch() and Appointments.find().fetch() in the console only the currently logged-in user (me) is available and there are no appointments available.
I expect to see two users (me and the patient) and one appointment available because that's the data that should be available after the waitOn has finished subscribing.
Am I missing something here?
EDIT----- Still doesn't make sense to me ------
When I change my route to this:
Router.route('/admin/appointment/:id', {
name: 'AppointmentShow',
waitOn: function(){
return [
Meteor.subscribe("userAppointment", this.params.id)
]
},
data: function(){
var appointmentId = this.params.id;
return {
appointment: Appointments.findOne(appointmentId),
// patient: Meteor.users.findOne(Appointments.findOne(appointmentId).patientId)
}
}
});
Appointments.findOne(appointmentId) returns an object:
{
_id: "23efref34qr2",
reason: "coughing",
patientId: "785g45g4f"
}
When my data function only returns
appointment: Appointments.findOne(appointmentId)
it works. But if I have it also return
patient: Meteor.users.findOne(Appointments.findOne(appointmentId).patientId)
I get an error message (can't read property 'patientId' of undefined.) Huh? It was just defined on the line above!
To clarify, I think you should be allowing your data function to run (and rerun when collections are populated), but be careful to make sure your function doesn't throw an error when it runs before data is available. This is a general Meteor pattern.
data: function(){
var appointmentId = this.params.id,
appointment = Appointments.findOne(appointmentId);
return { appointment: appointment,
patient: Meteor.users.findOne(appointment ? appointment.patientId : null) }
}
Sorry about the formatting, I'm doing this from an awful phone...
The problem seems to be that it runs the data function() before running the waitOn, and is therefore timing dependent. I have seen the same problem, and also had to check if the data was actually there.
i have followed this tutorial from Codelab and yeoman. When implemented right you are using local storage to store the TodoList. I have problems with setting up with my tests, to test if this works. This is what i've got so far:
'use strict';
describe('Controller: MainCtrl', function () {
// load the controller's module
beforeEach(module('yeoTodoApp'), module('LocalStorageModule'));
var MainCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope, $httpBackend) {
scope = $rootScope.$new();
MainCtrl = $controller('MainCtrl', {
$scope: scope
});
}));
it('should add items to the list', function () {
var beforeLength = scope.todos.length;
scope.todo = 'Test 1';
scope.addTodo();
var afterLength = scope.todos.length;
expect(afterLength-beforeLength).toBe(1);
});
it('should add items to the list then remove', function () {
var beforeLength = scope.todos.length;
scope.todo = 'Test 1';
scope.addTodo();
scope.removeTodo(0);
var afterLength = scope.todos.length;
expect(afterLength-beforeLength).toBe(0);
});
});
The error i get is
line 12 col 68 '$httpBackend' is defined but never used.
});
How would i write my unit tests to sit the local storage?
I think at the moment the idea is kind of mocking your local storage:
Write unit tests
For an extra challenge, revisit unit testing in Step 8 and consider
how you might update your tests now that the code is using local
storage.
Tip: It's not a straight forward answer and involves knowing about
mock services. Check out Unit Testing Best Practices in AngularJS,
specifically the Mocking Services and Modules in AngularJS section.
Things may have changed since this question was asked. Anyhow, here is my solution:
'use strict';
describe('Controller: MainCtrl', function () {
// load the controller's module
beforeEach(module('mytodoyoappApp'));
var MainCtrl,
scope,
localStorage, store;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
MainCtrl = $controller('MainCtrl', {
$scope:scope
// place here mocked dependencies
});
/*mock the localStorageService*/
store={};
localStorage = {
set: function(key, value) {
store[key] = value;
},
get: function(key) {
return store[key];
}
};
}));
it('should check the list length', function () {
expect(MainCtrl.todos.length).toBe(0);
});
it('should add items to the list', function () {
MainCtrl.todoadded = 'Test 1';
MainCtrl.addTodo();
expect(MainCtrl.todos.length).toBe(1);
});
it('should add then remove an item from the list', function () {
MainCtrl.todoadded = 'Test 2';
MainCtrl.addTodo();
MainCtrl.removeTodo(0);
expect(MainCtrl.todos.length).toBe(0);
});
it('should check that the localstorage is undefined before being set', function() {
var a=localStorage.get('todos');
expect(a).toBeUndefined();
});
it('should set and get the localstorage', function() {
localStorage.set('todos', ['Test 3']);
var a=localStorage.get('todos');
expect(a).toEqual(['Test 3']);
localStorage.set('todos', ['Test 4']);
var b=localStorage.get('todos');
expect(b).toEqual(['Test 4']);
});
});
your setup is correct now (after you removed $httpBackend from the arguments list)
Controller: MainCtrl should add items to the list then remove FAILED
this error is a simple test error, which means that your code somewhere doesnt work as expected (your second test fails)
i for myself would check todos length, and not the result of a mathematical operation.
i would write your tests the test like this:
it('should add items to the list then remove', function () {
scope.todo = 'Test 1';
expect(scope.todos.length).toBe(0);
scope.addTodo();
expect(scope.todos.length).toBe(1);
scope.removeTodo(0);
expect(scope.todos.length).toBe(0);
});
you use jasmine as a test-tool. jasmine logs on errors exactly which expectation fails, so you should get something like
expect '1' to be '0'
go from there!
I'm using NPM OAuth library because I can't get Meteor's to work. I get this error.
Error: Meteor code must always run within a Fiber. Try wrapping callbacks that you pass to non-Meteor libraries with Meteor.bindEnvironment.
It works if I remove the Collection.upsert line.
var OAuth = Meteor.require('oauth').OAuth;
var oa = new OAuth(null, null, consumer_key, consumer_secret, "1.0", null, "HMAC-SHA1");
var request = oa.post("https://stream.api.com/blah.json", access_token, access_secret);
request.on('response', function (response) {
response.setEncoding('utf8');
response.on('data', function(data) {
var j = JSON.parse(data)
Collection.upsert({symbol: j.symbol}, {last: j.last})
})
});
request.end();
I've read about Meteor.bindEnvironment and Meteor._wrapAsync but can't get it to work.
The Collection.upsert method works when embedded within a Fiber, while your callback for request.on is called arbitrarily, not wrapped with one. Try this:
request.on('response', function (response) {
new Fiber(function(){
response.setEncoding('utf8');
response.on('data', function(data) {
var j = JSON.parse(data)
Collection.upsert({symbol: j.symbol}, {last: j.last})
});
}).run();
});
I have a collection that is getting updated in an event handler and which is updating the collection and I would like to get the result of the update if it was a success or failure so I can do some logic based on its result. i.e. reset session values etc.
I have always been just testing the db action itself inside of an if block for inserts which worked fine however this does not seem to be working for update.
Template.customers_update.events({
'click a#cancel, click button#close' : function(event) {
event.preventDefault();
Session.set("editCustomer", false);
Session.set("customerId", null);
},
'click input[type=submit], submit form#create_customer' : function (event) {
event.preventDefault();
var customer_name = $("#customer_name").val();
var customer_address = $("#customer_address").val();
var customer_city = $("#customer_city").val();
var customer_state = $("#customer_state").val();
var customer_zip = $("#customer_zip").val();
var customer_phone = $("#customer_phone").val();
var customer_fax = $("#customer_fax").val();
var customer_eda = $("#eda_number").val();
var customer_duns = $("#duns_number").val();
if (Customers.update(Session.get("customerId"), {$set: {user_id: Meteor.user()._id, name: customer_name, address: customer_address, city: customer_city, state: customer_state, zip: customer_zip, phone: customer_phone, fax: customer_fax, eda_number: customer_eda, duns_number: customer_duns}})) {
console.log("Update Sucsess");
Session.set("editCustomer", false);
Session.set("customerId", null);
}
}
});
and in the server it is set to allow and return true
Customers.allow({
insert: function (userID, customer) {
return userID === customer.user_id;
},
update: function (userID, customer) {
return userID === customer.user_id;
},
remove: function (userID, customer) {
return userID === customer.user_id;
}
});
Use the third argument callback (docs)
callback Function
Optional. If present, called with an error object as its argument.
Your code is probably not working because the .update() only throws an exception on the server. From the docs:
On the server, if you don't provide a callback, then update blocks until the database acknowledges the write, or throws an exception if something went wrong. If you do provide a callback, update returns immediately. Once the update completes, the callback is called with a single error argument in the case of failure, or no arguments if the update was successful.
On the client, update never blocks. If you do not provide a callback and the update fails on the server, then Meteor will log a warning to the console. If you provide a callback, Meteor will call that function with an error argument if there was an error, or no arguments if the update was successful.
Change it to:
var updateQuery = {$set: {user_id: Meteor.user()._id, name: customer_name, address: customer_address, city: customer_city, state: customer_state, zip: customer_zip, phone: customer_phone, fax: customer_fax, eda_number: customer_eda, duns_number: customer_duns}}
Customers.update(Session.get("customerId"), updateQuery, function (error) {
//on error do this
});