After the model is designed, generated, and initialized with some data, the model should be tested to validate it further. In addition, writing tests by applying the techniques we learned in Chapter 3, Structuring Code with Classes and Libraries, is the best way to start learning dartling. Testing is done in the test/travel/impressions/travel_impressions_test.dart
file. The main
function creates a repository based on the JSON definition of the model and passes it to the testTravelData
function:
testTravelData(TravelRepo travelRepo) { testTravelImpressions(travelRepo, TravelRepo.travelDomainCode, TravelRepo.travelImpressionsModelCode); } void main() { var travelRepo = new TravelRepo(); testTravelData(travelRepo); }
The testTravelImpressions
function accepts the repository and the names of the domain and the model. In a group of tests, before each test, a setup is done to first obtain the three variables: models
, session
, and entries
. The models (here, only one) are obtained from the repository based on the domain's name. A session, with a history of actions and transactions of dartling, is created by the newSession
method of the models
object. The entries
variable for the only model is obtained from the model's object by using the model's name:
testTravelImpressions(Repo repo, String domainCode, String modelCode) { var models; var session; var entries; Countries countries; Country bosnia; Oid darivaOid, oid; Travelers travelers; group("Testing ${domainCode}.${modelCode}", () { setUp(() { models = repo.getDomainModels(domainCode); session = models.newSession(); entries = models.getModelEntries(modelCode); expect(entries, isNotNull); countries = entries.countries; travelers = entries.travelers; initTravelImpressions(entries); var code = 'BA'; bosnia = countries.singleWhereCode(code); darivaOid = bosnia.places.firstWhereAttribute('name', 'Dariva').oid; }); tearDown(() { entries.clear(); }); test("Not empty entries test", () { expect(!entries.isEmpty, isTrue); }); // code left out
The countries and travelers are the entry entities. The initTravelImpressions
function is called to initiate the model with some basic data. After the initialization, the country Bosnia is found in the countries
object based on its unique code. A single place, with the Dariva
name, is retrieved from the bosnia
object and its oid
is kept in the darivaOid
variable. The oid
attribute is inherited from dartling. It is a unique timestamp used as a system identifier in a collection of entities. Each test has an access to all these variables. After a test is run, the entries object is cleared so that the setUp
function may start from the empty model. The first test is run to show that the entries object is not empty after the setup. A single country is found by the singleWhereCode
method based on the inherited code attribute. In the Country
concept, the code
attribute is used. In the countries
object, each country must have a unique code:
test('Find country by code', () {
var code = 'BA';
Country country = countries.singleWhereCode(code);
expect(country, isNotNull);
expect(country.name, equals('Bosnia and Herzegovina'));
});
A single entity may be found by its user identifier. In the Country
concept, the user identifier is the name
attribute:
test('Find country by id', () {
Id id = new Id(countries.concept);
id.setAttribute('name', 'Bosnia and Herzegovina'),
Country country = countries.singleWhereId(id);
expect(country, isNotNull);
expect(country.code, equals('BA'));
});
If a concept has one attribute identifier (simple identifier), a creation of an ID object may be avoided by using a shortcut method called singleWhereAttributeId
:
test('Find country by name attribute id', () {
var name = 'Bosnia and Herzegovina';
Country country = countries.singleWhereAttributeId('name', name);
expect(country, isNotNull);
expect(country.code, equals('BA'));
});
The first entity with an attribute value equal to a value given to the firstWhereAttribute
method will then be obtained. If this attribute is an identifier, methods that use identifiers will perform faster:
test('Find country by name attribute', () { var name = 'Bosnia and Herzegovina'; Country country = countries.firstWhereAttribute('name', name); expect(country, isNotNull); expect(country.code, equals('BA')); });
If an entity is not a member of a collection of entities, a search
method will return null
:
test('Find country by name attribute id', () {
var name = 'Poland';
Country country = countries.singleWhereAttributeId('name', name);
expect(country, isNull);
});
The Place
concept has a composite identifier, composed of the country neighbor and the name
attribute. If only the name attribute is used, singleWhereAttributeId
will return null
:
test('Find country and (not) place by name id', () { var countryName = 'Bosnia and Herzegovina'; Country country = countries.singleWhereAttributeId('name', countryName); var placeName = 'Dariva'; Places places = country.places; Place place = places.singleWhereAttributeId('name', placeName); expect(place, isNull); });
The name
attribute may be used to find both a country and its place:
test('Find country and place by name attribute', () { var countryName = 'Bosnia and Herzegovina'; bosnia = countries.firstWhereAttribute('name', countryName); expect(bosnia, isNotNull); var placeName = 'Dariva'; Places places = bosnia.places; Place place = places.firstWhereAttribute('name', placeName); expect(place, isNotNull); expect(place.city, equals('Sarajevo')); });
However, the use of identifiers is recommended for performance reasons:
test('Find country and place by id', () { var countryName = 'Bosnia and Herzegovina'; bosnia = countries.singleWhereAttributeId('name', countryName); var placeName = 'Dariva'; Places places = bosnia.places; Id id = new Id(bosnia.concept); id.setParent('country', bosnia); id.setAttribute('name', placeName); Place place = places.singleWhereId(id); expect(place, isNotNull); expect(place.city, equals('Sarajevo')); });
The same results may be obtained by using more elegant method cascades. Note that the bosnia
variable is used in order to avoid searching for the country. Besides, in the last line, the place's oid
attribute is assigned to the oid
variable that will be used in the next test:
test('Find country and place by id (method cascades)', () { var placeName = 'Dariva'; Places places = bosnia.places; Id id = new Id(bosnia.concept) ..setParent('country', bosnia) ..setAttribute('name', placeName); Place place = places.singleWhereId(id); expect(place, isNotNull); expect(place.city, equals('Sarajevo')); oid = place.oid; });
In the model, the Country
concept is an entry. The relationship between the Country
and Place
concepts is internal. A place may be searched by its oid
attribute starting with the countries' entry, followed by the internal neighbors from the Country
concept:
test('Find place by oid by searching from countries down', { Place place = countries.singleDownWhereOid(darivaOid); expect(place, isNotNull); expect(place.name, equals('Dariva')); });
A specific method that does a job of finding an entity based on an identifier value may be used:
test('Find place by a specific method', () {
var placeName = 'Dariva';
Place place = bosnia.places.findById(placeName, bosnia);
expect(place, isNotNull);
expect(place.city, equals('Sarajevo'));
});
The findById
method is added to the Places
class in the lib/travel/impressions/places.dart
file:
Place findById(String name, Country country) { return singleWhereId(new Id(concept)..setAttribute('name', name).. setParent('country', country)); }
The city
attribute of the Place
concept is not required (not in bold in the Travel Impression model figure). Thus, it is not an identifier or a part of an identifier (not in italics). All the places in the city of Sarajevo may be selected by the selectWhereAttribute
method. The select
methods return a new collection of entities:
test('Select places in Sarajevo', () { Places places = bosnia.places.selectWhereAttribute('city', 'Sarajevo'), expect(places.length, greaterThan(0)); for (var place in places) { expect(place.city, equals('Sarajevo')); } });
A specific read-only property (with only the get
method) may be used in an anonymous function of the selectWhere
method to select a subset of entities:
test('Select places by function', () { Places places = bosnia.places.selectWhere((place) => place.old); expect(places.length, greaterThan(0)); for (var place in places) { expect(place.description, contains('old'))); } });
The old property with the get
method is added to the Places
class in the lib/travel/impressions/places.dart
file:
bool get old => description.contains('old') ? true : false;
The places of the bosnia
object are sorted by the city
attribute:
test('Sort places by city in Bosnia and Herzegovina', () { bosnia.places.sort( (place1, place2) => place1.city.compareTo(place2.city)); });
A new place is not added because the required values for the name
attribute and the country neighbor are missing. The corresponding error messages are added to the errors
property of the places
object:
test('Add place required error', () { Places places = bosnia.places; var placesCount = places.length; var place = new Place(places.concept); expect(place, isNotNull); var added = places.add(place); expect(added, isFalse); expect(places.length, equals(placesCount)); places.errors.display(title:'Add place required error'), expect(places.errors.length, equals(2)); expect(places.errors.toList()[0].category, equals('required')); expect(places.errors.toList()[0].message, equals('Place.name attribute is null.')); expect(places.errors.toList()[1].category, equals('required')); expect(places.errors.toList()[1].message, equals('Place.country parent is null.')); });
The error messages also appear in the console of the Dart Editor.
If we want to add a place that already exists according to its identifier, the add
method will not be successful:
test('Add place unique error', () { Places places = bosnia.places; var placesCount = places.length; var place = new Place(places.concept); expect(place, isNotNull); place.name = 'Dariva'; place.country = bosnia; var added = places.add(place); expect(added, isFalse); expect(places.length, equals(placesCount)); places.errors.display(title:'Add place unique error'), expect(places.errors.length, equals(1)); expect(places.errors.toList()[0].category, equals('unique')); });
In dartling, there are pre and posthooks for the add
and remove
methods. A pre-add hook may be used to validate a specific constraint that is not defined in the model:
test('Add place pre validation error', () { Places places = bosnia.places; var placesCount = places.length; var place = new Place(places.concept); expect(place, isNotNull); place.name = 'A new place with a name longer than 32 cannot be accepted'; place.country = bosnia; var added = places.add(place); expect(added, isFalse); expect(places.length, equals(placesCount)); places.errors.display(title:'Add place pre validation error'), expect(places.errors.length, equals(1)); expect(places.errors.toList()[0].category, equals('pre')); });
The specific preAdd
method is defined in the Places
class. The method is called by the add
method of dartling:
bool preAdd(Place place) { bool validation = super.preAdd(place); if (validation) { validation = place.name.length <= 32; if (!validation) { var error = new ValidationError('pre'), error.message = '${concept.codePlural}.preAdd rejects the "${place.name}" ' 'name that is longer than 32.'; errors.add(error); } } return validation; }
Finally, a new place is added:
test('Add place', () { Places places = bosnia.places; var placesCount = places.length; var place = new Place(places.concept); expect(place, isNotNull); place.name = 'Ilidza'; place.city = 'Sarajevo'; place.country = bosnia; var added = places.add(place); expect(added, isTrue); expect(places.length, equals(++placesCount)); });
The Travel Impressions app is further worked out to show all the data that was input: the countries and their places, the impressions, and the web links associated with each place, or the travelers and their impressions. These screens are not application-specific, but use the views, menu bars, and so on in the included dartling_default_app
library, showing the advantage of starting with a modeling framework.
18.191.254.44