Hi,
ich arbeite an der entgültigen Strategie zum Junit-Testing von Notes-Agents.
Was ich will:
1. Notes soll für Junit Tests von Notes Agenten nicht gestartet werden.
2. Junit Tests sollen relativ einfach geschrieben werden können.
Denke nun, dass Stubbing und nicht die verschiedenen Mock-Frameworks (wie JMock1 ) einfacher und flexibler ist.
Was verstehe ich unter Stubs?
Einfach Klassen, die die Notes Interfaces implementieren. Schliesslich läuft der gesamte öffentliche Zugriff auf die Notes Api ja über Interfaces.
Stubs für Notes Java Klassen lassen sich mit Eclipse sehr einfach erstellen:
Beispiel: Klasse DocumentStub erzeugen, die einfach das Interface lotus.domino.Document implementiert. Für alle Methoden des Interfaces lotus.domino.Document werden von Eclipse schon Methodenrümpfe erstellt. Die kann ich dann möglichst wiederverwendbar erweitern.
Kann man z.B. für getItemValue, getItemValueString und getItemValueInteger so implementieren:
public Vector getItemValue(String name) throws NotesException {
Vector ret = new Vector();
Object val = itemValueMap.get(name);
if (val != null) {
ret.add(val);
}
return ret;
}
public int getItemValueInteger(String name) throws NotesException {
Vector vec = getItemValue(name);
if (vec.size() == 0) return 0;
Object firstEntry = vec.get(0);
if (firstEntry instanceof Number) {
if ((firstEntry instanceof Double)|| (firstEntry instanceof Float)) {
return ((int) Math.round(((Double)firstEntry).doubleValue()));
} else {
return (int) ((Long) firstEntry).intValue();
}
} else {
return 0;
}
}
public String getItemValueString(String name) throws NotesException {
Vector vec = getItemValue(name);
if (vec.size() == 0) return null;
Object firstEntry = vec.get(0);
if (firstEntry instanceof String) {
return (String) firstEntry;
} else {
return null;
}
}
Das liesse sich dann für alle Projekte benutzen.
Die Stub Objekte besitzen eine private HashMap itemValueMap, in der die Feldwerte drinstehen:
private Map itemValueMap = new HashMap();
public void addItemValue(String name, Object value) {
itemValueMap.put(name, value);
}
Diese itemValueMap kann über addItemValue von aussen gefüllt werden.
Solche bei Bedarf immer mehr zu verfeinernden allgemein-verwendbare Notes Stub Objekte für Junit-Testing tu ich in ein eigenes Eclipse Projekte, dass ich dann problemlos in spezielle Projekte einbinden kann.
Oder ist das Quatsch? Ich halte die Idee zur Zeit für so großartig, dass ich mich wundere, warum kein anderer darauf gekommen ist.
Gruß Axel
Hier mein erster Agent auf meinem eigenen Notes:
Man beachte, dass die ganzen verwendeten Notes Klassen quasi "Nachbauten" sind. Für dieses Testsystem, braucht kein Notes auf dem Rechner vorhanden sein. Ok. Ausnahme ist noch die Klasse lotus.domino.NotesException. Ist ja auch ne beta ;D
Ging eigentlich ziemlich gut, wobei mein eigenes Notes nicht komplett ist.
Aber zumindest ich brauch für die meisten Java Agenten eben diese Klassen.
Weitere Klassen können natürlich noch hinzugefügt werden. RichText wird vermutlich nicht so einfach, aber wer weiss.
Ziemlich gut gelungen ist schon die Sortierung von Ansichten. 8)
package de.spintegration.mock.examples;
import java.util.Vector;
import java.util.Iterator;
import lotus.domino.NotesException;
import de.spintegration.notes.mock.AgentBase;
import de.spintegration.notes.mock.AgentInfo;
import de.spintegration.notes.mock.Database;
import de.spintegration.notes.mock.Document;
import de.spintegration.notes.mock.DocumentCollection;
import de.spintegration.notes.mock.Item;
import de.spintegration.notes.mock.MockFactory;
import de.spintegration.notes.mock.View;
import de.spintegration.notes.mock.ViewColumn;
/**
* @author ajanssen
*
*/
public class SimpleAgent extends AgentBase {
/* (non-Javadoc)
* @see de.spintegration.notes.mock.AgentBase#NotesMain()
*/
public void NotesMain() {
try {
System.out.println ("********************************");
System.out.println ("SESSION.GETNOTESVERSION :-). ");
System.out.println ("********************************");
System.out.println(session.getNotesVersion());
Database dbCur = agentContext.getCurrentDatabase();
View vw1 = dbCur.getView("ansicht1");
System.out.println ("********************************");
System.out.println ("ANSICHT");
System.out.println ("********************************");
System.out.println(vw1.prettyPrint());
Document docAfro = vw1.getDocumentByKey("Afrika");
Vector itemAfro = docAfro.getItems();
Iterator itItemAfro = itemAfro.iterator();
System.out.println ("********************************");
System.out.println ("FELDWERTE DES PER DOKUMENT-by-Key(Afrika) gefundenen Dokuments");
System.out.println ("********************************");
while (itItemAfro.hasNext()) {
String fieldName = itItemAfro.next().toString();
Item item = docAfro.getFirstItem(fieldName);
System.out.println(item.getName() + "-->" + item.getValues());
}
System.out.println ("********************************");
System.out.println ("KONGO-Dokument einfügen");
System.out.println ("UND BEACHTET WIE SICH DAS EINSORTIERT, KONTINENT UND LAND SIND SORTIERTE SPALTEN,s. main Methode oben");
System.out.println ("********************************");
Document docNewAfro = dbCur.createDocument();
docNewAfro.replaceItemValue("form", "maske5");
docNewAfro.replaceItemValue("kontinent", "Afrika");
docNewAfro.replaceItemValue("land", "Kongo");
docNewAfro.replaceItemValue("staedte", "Kinshasa");
docNewAfro.save();
System.out.println(vw1.prettyPrint());
System.out.println ("********************************");
System.out.println ("Dokument Collection-> GIBT JA JETZT 2 DOKUMENTE MIT KEY AFRIKA");
System.out.println ("********************************");
DocumentCollection colAfro = vw1.getAllDocumentsByKey("Afrika");
docAfro = colAfro.getFirstDocument();
while (docAfro != null) {
itemAfro = docAfro.getItems();
itItemAfro = itemAfro.iterator();
System.out.println ("********************************");
System.out.println ("Dokument IN Collection");
System.out.println ("********************************");
while (itItemAfro.hasNext()) {
String fieldName = itItemAfro.next().toString();
Item item = docAfro.getFirstItem(fieldName);
System.out.println(item.getName() + "-->" + item.getValues());
}
docAfro = colAfro.getNextDocument();
}
} catch (NotesException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main (String[] args) throws NotesException {
/*
* ERSTMAL MUSS DIE DATENBANK DEFINIERT WERDEN
* IST ZUGEGEBEN EIN WENIG MÜHSELIG.
* WERD ABER EINE MÖGLICHKEIT DER DEFINITION DER DATENBANK MIT XML ANBIETEN.
*/
MockFactory fac = MockFactory.getInstance();
Database parentDatabase = fac.getDatabase("test", "test.nsf");
// view definieren -> SORTIERUNG BEACHTEN.
View vw1 = fac.addView(parentDatabase, "ansicht1", "@all");
fac.addViewColumn(vw1, "form", "form");
fac.addViewColumn(vw1, "kontinent", "kontinent", ViewColumn.SORT_ASCENDING);
fac.addViewColumn(vw1, "land", "land", ViewColumn.SORT_ASCENDING);
fac.addViewColumn(vw1, "städte", "staedte");
// DOKUMENTE DIE VOR LAUF DES AGENTEN DA SIND.
Document doc1 = fac.addDocumentWithForm(parentDatabase, "maske1");
doc1.replaceItemValue("kontinent", "Europa");
doc1.replaceItemValue("land", "Deutschland");
Vector vecStädteDoc1 = new Vector();
vecStädteDoc1.add("Köln");
vecStädteDoc1.add("Hannover");
vecStädteDoc1.add("Frankfurt");
doc1.replaceItemValue("staedte", vecStädteDoc1);
doc1.save();
Document doc2 = fac.addDocumentWithForm(parentDatabase, "maske2");
doc2.replaceItemValue("kontinent", "Europa");
doc2.replaceItemValue("land", "Groß Britannien");
Vector vecStädteDoc2 = new Vector();
vecStädteDoc2.add("London");
vecStädteDoc2.add("Glasgow");
vecStädteDoc2.add("Manchester");
doc2.replaceItemValue("staedte", vecStädteDoc2);
doc2.save();
Document doc3 = fac.addDocumentWithForm(parentDatabase, "maske3");
doc3.replaceItemValue("kontinent", "Amerika");
doc3.replaceItemValue("land", "Argentinien");
Vector vecStädteDoc3 = new Vector();
vecStädteDoc3.add("Buenos Aires");
vecStädteDoc3.add("Cordoba");
vecStädteDoc3.add("Mendoza");
doc3.replaceItemValue("staedte", vecStädteDoc3);
doc3.save();
Document doc4 = fac.addDocumentWithForm(parentDatabase, "maske4");
doc4.replaceItemValue("kontinent", "Asien");
doc4.replaceItemValue("land", "Indien");
Vector vecStädteDoc4 = new Vector();
vecStädteDoc4.add("Mumbai");
vecStädteDoc4.add("Kalkutta");
vecStädteDoc4.add("Bangalore");
doc4.replaceItemValue("staedte", vecStädteDoc4);
doc4.save();
Document doc5 = fac.addDocumentWithForm(parentDatabase, "maske5");
doc5.replaceItemValue("kontinent", "Afrika");
doc5.replaceItemValue("land", "Maroko");
Vector vecStädteDoc5 = new Vector();
vecStädteDoc5.add("Marakesch");
vecStädteDoc5.add("Fes");
vecStädteDoc5.add("Rabat");
doc5.replaceItemValue("staedte", vecStädteDoc5);
doc5.save();
// set AGENTIFO
AgentInfo agInfo = new AgentInfo(fac);
agInfo.setCurrentDatabase(parentDatabase);
// AGENT STARTEN
SimpleAgent sa = new SimpleAgent();
sa.startup(agInfo);
}
}
und hier die Ausgabe:
********************************
SESSION.GETNOTESVERSION :-).
********************************
Happy Sandbox Notes, Release 0.4 Copper Edition|21st Century.
********************************
ANSICHT
********************************
Datenbank=test(test.nsf)
Ansicht=ansicht1, Selektionsformel=@all
form |kontinent|land |städte |
______|_________|_______________|____________________________|
maske5|Afrika |Maroko |Marakesch,Fes,Rabat |
______|_________|_______________|____________________________|
maske3|Amerika |Argentinien |Buenos Aires,Cordoba,Mendoza|
______|_________|_______________|____________________________|
maske4|Asien |Indien |Mumbai,Kalkutta,Bangalore |
______|_________|_______________|____________________________|
maske1|Europa |Deutschland |Köln,Hannover,Frankfurt |
______|_________|_______________|____________________________|
maske2|Europa |Groß Britannien|London,Glasgow,Manchester |
______|_________|_______________|____________________________|
********************************
FELDWERTE DES PER DOKUMENT-by-Key(Afrika) gefundenen Dokuments
********************************
staedte-->[Marakesch, Fes, Rabat]
form-->[maske5]
land-->[Maroko]
kontinent-->[Afrika]
********************************
KONGO-Dokument einfügen
UND BEACHTET WIE SICH DAS EINSORTIERT, KONTINENT UND LAND SIND SORTIERTE SPALTEN,s. main Methode oben
********************************
Datenbank=test(test.nsf)
Ansicht=ansicht1, Selektionsformel=@all
form |kontinent|land |städte |
______|_________|_______________|____________________________|
maske5|Afrika |Kongo |Kinshasa |
______|_________|_______________|____________________________|
maske5|Afrika |Maroko |Marakesch,Fes,Rabat |
______|_________|_______________|____________________________|
maske3|Amerika |Argentinien |Buenos Aires,Cordoba,Mendoza|
______|_________|_______________|____________________________|
maske4|Asien |Indien |Mumbai,Kalkutta,Bangalore |
______|_________|_______________|____________________________|
maske1|Europa |Deutschland |Köln,Hannover,Frankfurt |
______|_________|_______________|____________________________|
maske2|Europa |Groß Britannien|London,Glasgow,Manchester |
______|_________|_______________|____________________________|
********************************
Dokument Collection-> GIBT JA JETZT 2 DOKUMENTE MIT KEY AFRIKA
********************************
********************************
Dokument IN Collection
********************************
staedte-->[Kinshasa]
form-->[maske5]
land-->[Kongo]
kontinent-->[Afrika]
********************************
Dokument IN Collection
********************************
staedte-->[Marakesch, Fes, Rabat]
form-->[maske5]
land-->[Maroko]
kontinent-->[Afrika]
Mit Proxy hat das glaub ich nichts zu tun.
Durch die Persistenz in Notes, können unterschiedliche Versionen von Document-Objekten mit der gleichen DocUnid existieren.
Document docNew = db.createDocument();
docNew.replaceItemValue("form", "form1");
docNew.save();
Document docFromPersistedMem = db.getDocumentByUNID(docNew.UniversalID);
docNew.replaceItemValue("land", "Germany");
docNew und docFromPerstistedMem haben die gleiche UniversalID.
Das Objekt docNew hat am Ende des scripts ein Feld "land".
Das Objekt docFromPersistedMem hat dieses Feld dagegen nicht.
Erst durch ein docNew.save() würden beide Versionen wieder synchronisiert.
Das ist durch mein MockFramework zur Zeit schwer abzubilden, da es gar keinen persistenten Speicher hat. Die Dokumente sind rein in-Memory, dh. sie liegen in Database in (mehreren) Maps.
Zur Abbildung wäre es vermutlich am einfachsten, wenn ich eine Persistenzschicht einziehen würde. Eine Persistenzschicht zu simmulieren, dürfte auch machbar sein.
Allerdings bewege ich mich hier schon in Corner-Cases, die nicht Bestandteil der 1. Version sind.
Das zugegeben zur Zeit etwas schleppend weiterentwickelte Mockframework kann jetzt xml-Konfigurationsdateien zur Deklarierung der "Umgebung" einlesen.
Unter Umgebung verstehe ich so Sachen wie Datenbanken, Ansichten, Ordner, Dokumente.
Java-Agenten, LS2J Zeugs oder externe Javaprogramme, die mit dem Framework entwickelt und testbar gemacht werden sollen erwarten ja so etwas wie eine Umgebung.
Hier eine Beispiel-Konfigurationsdatei:
<?xml version="1.0" encoding="UTF-8"?>
<declaration>
<database title="music" path="music.nsf">
<view name="byYear" formula="form={music}">
<column title="year" formula="year" sort_policy="ASCENDING"
type="int" response="false" />
<column title="type" formula="type" />
<column title="artist" formula="artist" />
<column title="title" formula="title" />
</view>
<view name="all" formula="@all">
<column title="form" formula="form" sort_policy="ASCENDING" />
</view>
<folder name="wantHear" formula="form={music}">
<column title="artist" formula="artist"
sort_policy="ASCENDING" />
<column title="title" formula="title"
sort_policy="Ascending" />
<column title="year" formula="year" />
<column title="type" formula="type" />
</folder>
<profile form="profile">
<item name="aProfField">
wert
</item>
</profile>
<document form="music">
<item name="year" type="NUMBERS">1835</item>
<item name="artist">Robert Schumann</item>
<item name="type" sep=",">Clasic, Romantic</item>
<item name="title">Von fremden Ländern und Menschen</item>
</document>
<document form="music">
<item name="year" type="NUMBERS">1977</item>
<item name="artist">Joy Division</item>
<item name="type" sep=",">Pop</item>
<item name="title">Control</item>
</document>
</database>
</declaration>
Damit werden dann in der Umgebung die im xml deklarierten Datenbank(en), die Ansichten, die Profildokumente, die Ordner und die Dokumente angelegt.
Hier ist Testcode, der die obige xml einliest:
MockFactory fac = MockFactory.getInstanceXmlDecl("mockfactory.xml");
Session s = fac.getSession();
Database db = s.getDatabase("", "music.nsf");
View vw = db.getView("byYear");
;
System.out.println(vw.prettyPrint());
Hier ist das Ergebnis vom System.out des Testcodes:
Datenbank=music(music.nsf)
Ansicht=byYear, Selektionsformel=form={music}
year|type |artist |title |
____|________________|_______________|________________________________|
1835|Clasic, Romantic|Robert Schumann|Von fremden Ländern und Menschen|
____|________________|_______________|________________________________|
1977|Pop |Joy Division |Control |
____|________________|_______________|________________________________|
Sobald sich das xml stabilisiert, werd ich dafür ein xml-Schema schreiben. So kann leichter validiert werden, ob die Eingaben des Benutzers im xml ok sind.
Xml ist natürlich ein ziemlich geschwätziges Format, aber man kann da prima mit copy und paste arbeiten und es ist eben ziemlich selbsterklärend.
Ich denke auch darüber nach DXL einlesen zu können, das kommt aber später.