Wiki

Clone wiki

XALPI / DesigningUnitTests

As stated before, being able to unit test an application's business logic was one the primary goals of developing XALPI, by introducing abstractions that allow you to replace those abstractions during runtime with fakes / mocks / stubs / whatever.

So the first thing you should know is that for each XEOModel in your application an interface with the name of the model was generated, and two implementations were also generated

  • One implementation that uses boObject instances to support the abstraction (production code)
  • One implementation that uses JSON Objects to support the abstraction.

So if the XEOModel is Car, you have the following:

#!java
public interface Car extends BaseModel;
public class CarImpl implements Car;
public class CarJSON implements Car;

Let's imagine you have the following method in a class

#!java

public class CarChecker(){

public boolean isCarMercedes(Car carToCheck){
   if ("Mercedes".equals(carToCheck.getName())
       return true;
   else
       return false;
}

}

We begin the unit test by creating a method using JUnit, something like the following:

#!java
@Test
public void car_is_mercedes(){
    Car mercedes = ; //Create a Car that is a mercedes somehow
    CarChecker checker = new CarChecker();
    boolean value = checker.isCarMercedes(mercedes);

    Assert.assertTrue("Car should be a mercedes",value);
}

So the only question here is... how do I create a Car that does not require the XEO Application to be up? Simple, you use the JSON implementation like the following;

CarMercedes.txt

#!javascript
{ sys_classname : 'Car' , name : 'Mercedes' }

And then you make something like:

#!java

private JSONObject loadCar(){
    JSObject obj = readFileAsJSON("CarMercedes.txt");
}

@Test
public void car_is_mercedes(){
    Car mercedes = new CarJSON(loadCar());
    CarChecker checker = new CarChecker();
    boolean value = checker.isCarMercedes(mercedes);

    Assert.assertTrue("Car should be a mercedes",value);
}

Each XEOModel has a JSON Implementation with a constructor that accepts a JSON Object as parameter. The JSON Object has a required field which is the sys_classname (which should have the Model name as a string). It can also have the following "System" attributes (which will be familiar to anyone using the XEO API)

#!javascript
{
  "sys_exists" : true/false  //Whether or not the object is supposed to be persisted
, "sys_changed" : true / false //Whether or not the object is supposed to be changed
, "sys_disabled" : true / false // Whether or not the object is supposed to be disabled
, "sys_dtcreate" : "10-06-2008 13:54:34" //Date of creation
, "sys_dtupdate" : "12-04-2009 23:34:14" //Date of last update
, "sys_classname" : "Car" //Type of XEO Model
, "parent$" : { ANOTHER OBJECT DEFINITION } //The parent (if any)
, "sys_obj_errors" : [ "Error1", "Error2", "Error3"] //Errors in the object
, "sys_att_errors" : { "attributeName" : "attributeError", "attributeName2" : "attributeError2" }  //Attribute errors
, "BOUI" : 11111 //The BOUI of the instance
}

For the remaining attributes you use the following (notice the brand and colors attributes (an object relation and collection relation, respectively:

#!javascript
{
   "name" : "Mercedes"
,  "year" : 2009
,  "type" : "0"
,  "brand" : { "sys_classname" : "ModelBrand" , "name" : "Mercedes"}
,  "colors" : [ {"sys_classname" : "Color" , "name" : "Red" }, {"sys_classname" : "Color" , "name" : "Blue" }]
}

Creating and Loading Objects with the JSON Implementation

If you want to create/load instances, you use the MODEL_NAMEManager class. If you want to "fake" that creation/loading process you need to do the following: In each Manager class there's this method available:

#!java
public static void setFactory(PatientFactory newFactory);

You can use that to change the existing Factory, for instance, you switch the default CarFactory with a JSON Implementation, like this (example is a JUnit test):

#!java

@Test
public void test_create_new_car(){

    CarFactoryJSON factory = new CarFactoryJSON();
    factory.addModel(createNewPatientFromJSONFile()); //Instance has BOUI = 300
    CarManager.setFactory(factory);

    Car loaded = CarManager.load(300);
    Assert.assertTrue(loaded.getName().equals("Test"));
}

Each JSONFactory has several methods to allow you to manipulate the process, and you can also implement your own factories :)

Listing with the JSON Implementation

In case you want to unit test methods that require access to the list API, you can setup a "database" for the lists like the following:

#!javascript

[
    {
        "description" : "Empty CarList"
        , "modelName" : "Car"
        , "where" : "name = ?" 
        , "args" : {   "name" : "Mercedes" }
        ,"data" : []        
    }
    ,{
        "description" : "Car with one element"
        , "modelName" : "Car"
        , "where" : "name = ? and date = ?" 
        , "args" : {
                      "name" : "BMW"
                    , "date" : "23-07-2013 00:00:00"      
                 }
        ,"data" : [ 
                    {
                          "sys_classname" : "Car"
                        , "dateOpen" : "23-07-2013 00:00:00"
                        , "name" : "BMW" 
                        , "file" : { "sys_classname" : "ModelBrand", "name" : "BMW" }       
                    }
                ]       
    }   
]
Then you need to setup the SelectFactory for it to use the database.

#!java
String database = readFileFromString("CarDatabase.txt");
SelectFactory selectFactory = new SelectFactoryJSON(new JSONArray(database));
Select.setFactory( selectFactory );
The way the "database" works is very simple. It tries to match the modelName, where and arguments provided in the queries of your methods to the ones in the database. If you make the following query against the previous database:
#!java
Select.Car().build();
It will not match anything and return an empty list, however if you do:
#!java
Select.Car().where("name = ?").args("Mercedes").build();
It will match the first listing in the database (although the list is empty). The mechanism is always the same. For queries via the API to match the listings in the database you have to make sure those queries match the modelName, where and args values in some of the listings. The reason for the modelName parameter is so that you can have queries for multiple models. The reason for the where and args parameters is to allow you to execute the same queries with multiple arguments and return different lists.

Warning about Listing Databases

It's not prudent to have a BIG database with all the listings you want to execute in tests (like a "test database"), it's better to have small "databases" with just the data for your specific test. If not it will quickly become a mess, believe me :)

Checking instances that were created

If you have a method that creates a given instance of a model and want to check if it's well created, you can do the following:

#!java

public class MercedesCreator(){

//Method to test
public void createCar(){
   Car car = CarManager.create();
   car.setName("Mercedes");
   car.update();
}
}
#!java

@Test
public void carCreated(){

   CarFactoryJSON carFactory = new CarFactoryJSON();
   CarManager.setFactory(patientFactory);

   MercedesCreator c = new MercedesCreator();
   c.createCar();

   List<Car> created = carFactory.getCreatedInstances();      
   if (created.isEmpty()){
     Assert.fail("Should have at least one");
   } else {
     Car createdCar = created.get(0);
     Assert.assertTrue(createdCar.getName().equals("Mercedes"));
   }



}

Summary of generated classes for a given XEO Model (Car.xeomodel as example)

#!java

//Model
public interface Car extends BaseModel; //Interface that represents the model
public class CarImpl implements Car; //Class that uses a boObject to support the abstraction
public class CarJSON implements Car; //Class used for unit testing Car

//Collection of Model
public interface CarCollection extends Iterator<Patient>,Iterable<Patient>; //Interface that represents a Bridge/Collection of Car
public class CarCollectionImpl implements CarCollection; //Class that uses a bridgeHandle to support the collection
public class CarCollectioNJSON implements CarCollection; //Class for unit testing Car bridges/collection

//Model Loading/Creating
public interface CarFactory;
public class CarFactoryImpl implements CarFactory; //Creates and Loads boObject instances as normally
public class CarFactoryJSON implements CarFactory; //Create and Load JSON instances for unit testing
public class CarManager; //Factory class that uses the factory implementation to load/create instances

//Listing of instances
public interface CarList extends Iterable<Car>; //Interface to represent a list of instances
public class CarListImpl implements CarList; //Class that uses a boObjectList to support the list
public class CarListJSON implements CarList; //Class that uses a List<Car> or a JSONArray to support the list (for unit testing)

//Creating Lists of instances (through the Select class)
public interface CarListBuilder; //Interface to allow the use of the builder pattern to create a new CarList
public class CarListBuilderImpl implements CarListBuilder; //Implementation using boObjectLists
public class CarListBuilderJSON implements CarListBuilder; //Implementation using JSON Arrays (for unit testing)

Updated