Das Mocking von Services war lange eine sehr komplizierte Sache. Mit Javassist oder speziellen Implementierungen konnte man es lösen, aber es war oft einfach nicht schnell und einfach zu benutzen. Mit Mockito kann man pro Test die Method-Calls einer Klasse überschreiben und so relativ einfach auch Test für Klassen mit Dependency Injection schreiben. Wenn man eine Service-Klasse testen möchte, die auf ein DAO zurückgreift um Daten aus der Datenbank zu laden, kann man nun einfach die DAO-Klasse mit Mockito so manipulieren, dass sie bestimmte Daten liefert ohne auf die Datenbank zu gehen und so hat man die volle Kontrolle über die Input- als auch die Output-Daten.
import org.junit.*;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import static org.mockito.Mockito.*; //to use when() without static class
public class ModelTest {
//will always return a model with id = 0 (not existing), simulating an empty/not existing database
@Mock
private TestModelDAO dao;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Before
public void setup(){
//creating an existing dummy for id 23, so 23 will always return a valid model
TestModel dummy = new TestModel();
dummy.setId(23);
dummy.setName("blubb-23");
when(this.dao.getModel(23)).thenReturn(dummy);
}
@Test
public void getModelSimple(){
TestModel model = (new TestModelDAO()).getModel(42);
Assert.assertEquals(0, model.getId());
}
@Test
public void getModelComplexId(){
TestModel model = this.dao.getModel(23);
Assert.assertEquals(23, model.getId());
}
@Test
public void getModelComplexName(){
TestModel model = this.dao.getModel(23);
Assert.assertEquals("blubb-23", model.getName());
}
@Test
public void getModelDIExample(){
//Testing a service with constructor dependency injection
TestService service = new TestService(this.dao);
String upperName = service.getUpperCaseName(23); //name for id 23
Assert.assertEquals("BLUBB-23", upperName);
}
}
An sich sollte man solche Methoden wie getUpperCaseName nie so schreiben und immer die fertig geladene Entität rein reichen. Aber gerade bei älteren Legacy-Code findet man solche Dinge oft. Auch Tests mit fehlenden Request oder ähnlichen kann man so durchführen, ohne direkt eine gesamte HTTP-Umgebung nachbauen zu müssen.
Mit SDKMAN kann man relativ einfach SDKs im System verwalten ohne immer alles selbst per Hand oder aus Repositories installieren zu müssen.
Einmal gucken welche Versionen verfügbar sind:
sdk list java
Dann installieren:
sdk install java 11.0.1-open
Gradle geht damit auch ganz einfach:
sdk install gradle 5.0
Ich musste mich damit beschäftigen wie man eine kleine REST-API mit Python erstellt. Django macht dabei an sich bei jeden Pups bei mir Probleme und war doch sehr umständlich, weil man sich noch mit dem gesamten MVC-Pattern darin beschäftigen musste. Nachdem es auch mit dem ORM schwieriger wurde (im Vergleich zu Spring Boot mit JPA/Hibernate) dachte ich mir, es müsse doch auch für Python was modernes geben. So kam ich zu Turbo Gears 2 und das macht schon mal genau was ich wollte. Einfach, schnell und übersichtlich.
from tg import expose, TGController, AppConfig
import jsonpickle
# --
class TestEntity(object):
def __init__(self, id ,name, sub):
self.id = id
self.name = name
self.sub = sub
class TestSubEntity(object):
def __init__(self, value):
self.value = value
# --
class RootController(TGController):
@expose("json")
def index(self):
test = TestEntity(42, 'blubb', TestSubEntity('sub-blubb'))
return jsonpickle.encode(test, unpicklable=False)
@expose("json")
def method2(self):
test = TestEntity(23, 'blubb-2', TestSubEntity('sub-blubb-2'))
return jsonpickle.encode(test, unpicklable=False)
# --
config = AppConfig(minimal=True, root_controller=RootController())
application = config.make_wsgi_app()
from wsgiref.simple_server import make_server
print("Serving on port 8090...")
httpd = make_server('', 8090, application)
httpd.serve_forever()
Mit Turbo Gears 2, Spring Boot und Meecrowave kann wirklich schnell und einfach Microservices erstellen und vieles des Overheads alter Zeiten ist einfach nicht mehr da bzw wurde so gut versteckt, dass man sich rein auf den Code und die Logik konzentrieren kann. Welche Lösung man da nimmt ist Geschmackssache. Von der Codestruktur her sieht alles an sich fast 100% gleich aus.
Bei Python sehen Flask, Bottle und Hug auch interessant aus
Unit-Tests sind ja immer ganz schön, um Berechnungen und andere atomare Logiken zu testen. Berechne ich den Preis richtig? Funktioniert das Regex- oder XPath-Pattern noch? Kann man alles mit Unit-Tests super und zu zuverlässig testen. Wenn es kann aber um Workflows oder Benutzerführung geht wird es schwer. Auch muss ein UX-Spezialist nach jeder neuen Version wieder testen, ob die von ihm festgelegten Wege und Regeln noch genau so funktionieren und der Benutzer auch das zusehen bekommt was er soll und nicht plötzlich in einer falschen Ansicht landet.
Hier kommt Mink ins Spiel. Mink wird verwendet von Behat und das ist ein Behavior Driven Development Framework. Wie auch bei Unit-Tests wird erst formuliert, was wo passieren soll und wie der Besucher sich auf der Seite bewegt und was er wann zu sehen bekommt.
Auf meinem Blog soll ein Besucher mit der Hauptansicht des Blogs starten und wenn er nach "rfid" sucht, den Post mit "POS-Plugin Cashless-Payment Demo" finden. Außerdem soll er beim PoE-Kameras Post Kommentare hinterlassen können.
Behat aufsetzen ist nicht immer ganz einfach, weil es beim Composer teilweise zu Versionsproblemen kommen kann. Deswegen hier mein kleines Grund-Setup.
composer.json
{
"name": "hp/behat_test",
"authors": [
{
"name": "Hannes Pries",
"email": "hp@hannespries.de"
}
],
"require": {
"behat/behat": "*",
"behat/mink-extension": "*",
"behat/mink-browserkit-driver": "*",
"behat/mink-goutte-driver": "*",
"behat/mink-selenium2-driver": "*"
},
"config": {
"bin-dir": "bin/"
}
}
Dazu kommt die behat.yml im Projekt-Root (Behat 3):
default:
suites:
default:
contexts:
- Behat\MinkExtension\Context\MinkContext
extensions:
Behat\MinkExtension:
base_url: 'https://www.hannespries.de'
files_path: 'vendor'
goutte: ~
und jetzt kommt der Test unter features/home.feature:
Feature: Home
Scenario: Starting with blog index page
Given I am on "/"
Then I should see "Latest Entries"
Scenario: Search for RFID
Given I am on "/index.php?page=Blogs&sub=search"
When I fill in "pattern" with "rfid"
And I press "search"
Then I should see "POS-Plugin Cashless-Payment Demo"
Scenario: Display comment-fields in blog-post
Given I am on "/idx-blog--berwachtungskameras-mit-poe-und-nas.html?page=Blogs&sub=viewBlog&blogId=465"
Then I should see "write comment:"
Scenario: Display comment-fields in blog-post 2
Given I am on "/idx-blog--berwachtungskameras-mit-poe-und-nas.html?page=Blogs&sub=viewBlog&blogId=465"
Then I should not see "Not able to write comment"
Und das war es auch schon. Jetzt kann man den Test über bin/behat features/home.feature starten und es wird alles einmal durch getestet.
Eine gute Übersicht über die Syntax findet man hier https://gist.github.com/mnapoli/5848556.
Und eine Beispiel-Projekthier:
https://github.com/jaffamonkey/behat-3-kickstart
Allein mit dem wenigen Syntax den ich dort verwendet habe kommt man schon relativ weit. Wenn man stark JavaScript-basierte WebApps testet gibt entsprechende Erweiterungen, um auch direkt selbst mit JavaScript dinge triggern oder Events abfeuern zu können.
Vor einiger Zeit hatte ich eine PLZ-Suche implementiert, bei der Locations bis zu einer bestimmten Distanz zu einer 2. per PLZ identifizierten Location als Ergebnismenge heraus gesucht wurden.
Es wurde also Also eine Distanz zwischen 2 per GPS-Koordinaten beschrieben Locations berechnet. Ich habe das in PHP erledigt, weil die Locations der zu findenen Orte in einer JSON-Datei vorlagen und ich mir den Import in die MySQL-DB ersparen wollte. Die Orte per PLZ kamen aber aus der MySQL-DB, wobei immer der erste Ort mit der PLZ die Koordinaten geliefert hat.
/**
* https://www.geodatasource.com/developers/php
*
* @param $lat1
* @param $lon1
* @param $lat2
* @param $lon2
* @param string $unit
* @return float
*/
public function calcDistance($lat1, $lon1, $lat2, $lon2, $unit = 'K'): float
{
$theta = $lon1 - $lon2;
$dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta));
$dist = acos($dist);
$dist = rad2deg($dist);
$miles = $dist * 60 * 1.1515;
$unit = strtoupper($unit);
if ($unit == 'K') {
return ($miles * 1.609344);
} else if ($unit == 'N') {
return ($miles * 0.8684);
} else {
return $miles;
}
}
Auf der Seite findet man auch Code für PL/SQL (Oracle), Java und JavaScript. Nur leider für MySQL nicht. Die findet man aber hier. Im Grunde kann man sich das aber für jede Sprache selbst ableiten, weil es normale Mathematische Berechnungen sind ohne dass zusätzliche Libs oder Klassen nötig wären (die es aber auch gibt und diese Logik kapseln).
Jeden Falls sind solche Berechnungen an sich ganz einfach und performant, so dass man sich alles schnell selbst schreiben kann und nicht auf 3rd Party Lösungen angewiesen ist.
Wenn man sich für Shopware ein Plugin kauft, kann es sein, dass man die Daten genau so vorfindet, wie man sie braucht, aber möchte das dort verwendete Template ersetzen oder die Daten in einem Template verwenden, das im Plugin noch gar nicht vorgesehen war.
Dafür kann man sich ein eigenes kleines Plugin schreiben. Das geht in 5 Minuten. Wir schreiben uns das Plugin TESTNotLoggedIn und blenden damit den Newsletter in der Footer Navigation aus.
Ins Verzeichnis TESTNotLoggedIn kommt die Datei TESTNotLoggedIn.php:
namespace TESTNotLoggedIn;
use Shopware\Components\Plugin;
class TESTNotLoggedIn extends Plugin{
public static function getSubscribedEvents()
{
return [
'Enlight_Controller_Action_PostDispatchSecure_Frontend' => 'addTemplateDir',
'Enlight_Controller_Action_PostDispatchSecure_Widgets' => 'addTemplateDir',
];
}
public function addTemplateDir(\Enlight_Controller_ActionEventArgs $args)
{
try {
$args->getSubject()->View()->addTemplateDir($this->getPath() . '/Resources/views/');
}
catch(\Exception $e){
//TODO
}
}
}
jetzt kommt das Verzeichnis Resources/views/ dazu und die Datei plugin.xml:
<?xml version="1.0" encoding="utf-8"?>
<plugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/5.2/engine/Shopware/Components/Plugin/schema/plugin.xsd">
<label lang="de">TEST Not Logged In</label>
<label lang="en">TEST Not Logged In</label>
<version>0.1</version>
<link>https://www.hannespries.de</link>
<author>Hannes Pries</author>
<compatibility minVersion="5.2.0" />
<requiredPlugins>
<requiredPlugin pluginName="HPrGlobalCustomerData" minVersion="1.3"/>
</requiredPlugins>
</plugin>
Hier wird das Plugin, das wir erweitern wollen, angegeben. Ich verwendet das kostenlose "Globale Kunden Smarty-Variablen". Nun können wir einfach unsere Templates im views-Verzeichnis anlegen und vorhandene erweitern.
Als Beispiel kommt hier Resources/views/frontend/index/footer-navigation.tpl
{extends file="parent:frontend/index/footer-navigation.tpl"}
{block name="frontend_index_footer_column_newsletter"}
{if !$customer_loggedIn}
{$smarty.block.parent}
{/if}
{/block}
Damit ist das Plugin schon fertig und kann installiert werden. Der Newsletter Bereich im Footer ist nun nur sichtbar, wenn man eingelogged ist.
Das Plugin "Eigenschaften in Artikel-Listing" bietet z.B. auch an in der Detailseite die Daten nur als Smarty-Variable bereit zu stellen, damit man selbst direkt die Darstellung implementieren kann und nicht noch eine vorhandene unpassende anpassen oder entfernen und ersetzen muss.