Ankündigung

Einklappen
Keine Ankündigung bisher.

Unit Testing Frage

Einklappen

Neue Werbung 2019

Einklappen
X
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • Unit Testing Frage

    Hi,

    da ich Testen immer recht konsequent aus dem Weg gegangen bin, es aber doch gerne machen würde, hab ich mich in den letzten Tagen etwas mehr damit beschäftigt.

    Folgende Frage aber:
    Man geht von folgendem Quellcode aus:

    PHP-Code:
    class Foobar {
        
        public function 
    doSomething(Foo $fooBar $bar) {
            
    $obj = (new SomeObject())->foo();

            
    $foo->method();
            
    $bar->method();

            return ;
        }


    Foo und Bar sind einfach zu testen. Ich erstelle mir einen Mock zu den Objekten und DI macht den Rest. Ich kann dann auch testen, ob $foo und $bar jeweils den Call auf method() bekommen - wie aber ist das bei $obj?

    Besteht auf "auf Testen ausgelegtes Programmieren" tatsächlich komplett aus DI und verzichtet gänzlich auf sowas?

    D.h. ich würde auch SomeObject der Funktion übergeben, mocken und es dann testen können. Was aber wenn ich mehrere von den Objekten brauche? Dann muss doch die komplette Implementierung von SomeObject anders aussehen (dann hätte sie wohl irgendeine instance()-Methode die mir jedes mal eine neue Instanz zurückliefert). Ist das Sinn der Sache?

    Danke

  • #2
    Wenn du tatsächlich mehre Objekte übergeben möchtest, dann musst du das eben auch tun. Das hat ja erstmal nichts mit DI zu tun, sondern einfach mit Parameterübergabe. Wenn du die Mock-Objekte aber nur in dieser Funktion brauchst, hindert dich aber auch nichts daran diese gleich vor Ort, also in der Testmethode selbst, zu erstellen.

    In einem so allgemeinen Fall wie deinem kann man eben nicht sagen wo du die Objekte erstellen/mocken solltest.

    Kommentar


    • #3
      ich würde dir empfehlen, PHPUnit zu nutzen.
      http://phpunit.de/

      Da haste auch ne Mocking Bibliothek.
      Ansonsten wäre Mockery (https://packagist.org/packages/mockery/mockery) auch ne option

      LG
      https://github.com/Ma27
      Javascript Logic is funny:
      [] + [] => "", [] + {} => object, {} + [] => 0, {} + {} => NaN

      Kommentar


      • #4
        Also ganz grundsätzlich mal erfordert eine testbare Applikation tatsächlich einen gewissen Qualitätsstandard (nicht "Den", sondern einen Eigenen, Bestimmten).

        Eigentlich kann man TDD recht einfach zusammenfassen:
        • Du schreibst einen Test bevor du den Code schreibst, den du eigentlich testen willst.
        • Du bist zu einem gewissen Grad genötigt, deine Komponenten mit wenig Abhängigkeiten zu entwickeln.
        • Alle Anhängigkeiten zu einer Komponente werden im Idealfall über DI vermittelt, damit du alle wichtigen und theoretisch auftretenden Fälle durch Injektion abbilden kannst.
        • Deine Tests und Testdaten sind deterministisch!
        • Du testest ständig - idealerweise lässt du ständig testen (Stichwort CI)


        (Qualität) Ganz ehrlich. Man kann nicht einfach anfangen testgetrieben zu entwickeln. Das ist eine Wissenschaft für sich. Zumal im PHP-Umfeld noch erschwerend hinzukommt, dass man ständig Dinge testet, die die Sprache selbst nicht sicherstellen kann (was passiert, wenn man da dann ein "hallo" statt einer Zahl übergibt?). TDD-Software muss unter Berücksichtigung gewisser Architekturstandards gedeihen, sonst ist die Architektursprache zu verdiversifiziert. Heisst: Wenn man jede Komponente einer Applikation erst mal wieder erlernen muss, bevor man sie ändern kann, kann das viel Zeit kosten und erhöht das Fehlerrisiko weiter.

        (Vollständigkeit) Grundsätzlich musst du erst mal eine Strategie entwickeln, wie man eigentlich testgetrieben entwickeln kann. Das kann eine Applikationsarchitektur stark beeinflussen - muss es aber nicht. Gewisse Abläufe lassen sich nur sehr mühsam als Test ausdrücken. Wenn man als Entwickler nicht die notwendige Disziplin hat und unter chronischem Zeitmangel leidet findet schnell Gründe, scheinbar obligatorische Abläufe nicht in den Testumfang aufzunehmen.

        (Korrektheit) Dann ist es auch gar nicht so einfach einen Test zu schreiben. Was soll eigentlich getestet werden? Eine erste Idee hat sich häufig dazu schnell gebildet. Aber dann auch konsequent zu bleiben und jeden theoretischen Fall zu erfassen kann sehr zeitintensiv sein. Und würden alle Entwickler immer Perfekt arbeiten, würde es auch keine Bugs geben - es gibt sie nun mal. Es gibt sie auch in Tests. Ein Test stellt also nie sicher, dass eine Komponente funktioniert sondern, dass sie unter den im Test hinterlegten Bedingungen funktioniert. Und die können dann sogar im schlimmsten Fall falsch sein.

        (Zeit) Im besten Fall nimmt TDD noch mal die gleiche Zeit in Anspruch, die bei der Entwicklung der zu testenden Komponente investiert wurde. Das ist einfach erklärt: Eine Komponente ist dann gut, wenn sie mit möglichst überschaubaren Umfang eine Menge leistet. Beispiel: URL-Builder. Braucht man nicht viel Code für - php bringt hier schon einiges aus dem Stand mit. Ein Test für so einen Builder kann aber ewig groß sein. Und da ist dann auch das nächste Problem versteckt: Wann testet mal zu viel? Zu viel testet man dann, wenn Tests das Refactoring einer Applikation behindern. Das passiert eben dann, wenn man Komponenten hat, die zu viele Zuständigkeiten haben. Hat man diese Zuständigkeiten erst mal alle getestet, dann braucht man ggf. auch lange um eine Komponente an sich geänderte Gegebenheiten anzupassen. Daher lernt man als TDD-Entwickler auch schnell die SOLID-Prinzipien kennen und schätzen

        (Fertige Komponenten) Wann immer möglich, solltest du (natürlich immer unter Berücksichtigung der gegebenen Lizenz) getestete Komponenten anderer Entwickler einsetzen. Ich habe in meinem Leben (meist zu Lernzwecken) ewig oft das Rad neu erfunden. Eine gewisse eigene Kompetenz sollte man beim Einsatz fremder Komponenten stets haben. Nur weil es da was auf Github gibt, was auf den ersten Blick passig klingt, muss es nicht unbedingt gut zu dem passen, was man selbst vor hat. Das mindeste ist, dass man sich durch fremde Komponenten keine Bugs oder gar Sicherheitslücken einhandelt. So sollten auch bereits fremdgetestete Komponenten in Form von Integrationstests berücksichtigt werden.

        (Tools) Braucht man PHPUnit unbedingt zum Unittesten? Jaaain...
        Wenn du eine professionelle IDE wie ZendStudio oder PHPStorm einsetzt, dann erlebst du eine enge Verzahnung von PHPUnit und deiner IDE. Kann spassig sein. Muss aber nicht. Fang auf jeden Fall nicht mit etwas eigenem an. Finde immer erst heraus, ob du bestehende (und von anderen getestete) Tools nicht einsetzen kannst. PHPUnit bringt ein nicht gerade schönes (naja, für PHP an sich sehr wohl schönes) Interface mit. Ich mag es nicht - aber ich setze es trotzdem überall ein.

        Kommentar


        • #5
          @rkr,

          super Beitrag

          PHPUnit bringt ein nicht gerade schönes (naja, für PHP an sich sehr wohl schönes) Interface mit. Ich mag es nicht
          Nur mal so aus Interesse: was genau gefällt dir nicht?

          LG
          https://github.com/Ma27
          Javascript Logic is funny:
          [] + [] => "", [] + {} => object, {} + [] => 0, {} + {} => NaN

          Kommentar


          • #6
            Puh, vieles. Die Codebasis ist halt immer noch 5.2. Es fühlt sich einfach überholt an. Schau dir mal an, was sich auf dem Markt so tut. Da ist viel Raum für Inspiration. Ein gutes Beispiel ist Karma.

            Kommentar


            • #7
              Ich glaube, ich habe ein falsches Bild über meine Kenntnisse vermittelt, auch wenn der Beitrag sehr informativ und interessant ist Danke!

              Meine Tests laufen bereits über CI (genauer: GitLab CI), ich nutze phpunit und meine Applikationen holen sich die meisten Abhängigkeiten über DI. Mein Verständnisproblem ist folgendes:

              Alle Anhängigkeiten zu einer Komponente werden im Idealfall über DI vermittelt, damit du alle wichtigen und theoretisch auftretenden Fälle durch Injektion abbilden kannst.
              Ich nehme mal das Originalbeispiel her:
              Code:
              <?php
              class DashboardController extends BaseController {
              
                  public function __construct(
                      ProjectRepository $projects,
                      UserRepository $users,
              		FeedAggregator $feed
                  )
                  {
                      $this->projects = $projects;
                      $this->users    = $users;
              		$this->feed		= $feed;
                  }
                  
                  public function index()
                  {
                      $userProjects    = $this->projects->getProjectsWhereUserIsContributor();
                      $currentlyOnline = $this->users->getCurrentlyOnline();
                      $feed            = $this->feed->get();
                      
                      return View::make('core.dashboard')
                                  ->with('feed',              $feed)
                                  ->with('user_projects',     $userProjects)
                                  ->with('currently_online',  $currentlyOnline);
                  }
                  
              }
              Recht simpler Controller, wobei alle Abhängigkeiten über DI laufen. Mein FeedAggregator kann aber unter Umständen mehrfach verwendet werden, d.h. dass ich verschiedene Feeds auf einer Seite habe.

              Die Frage jetzt, welchen Weg ich jetzt nehme falls das wirklich mal sein sollte (das ich mehrere Instanzen vom FA brauche):

              Weg 1:
              Ich erzeuge den FeedAggregator direkt in der Methode via:
              Code:
              $feed = (new FeedAggregator())->get();
              Weg 2:
              Ich baue den FA um, so dass er über eine Methode verfügt, die mir eine Instanz liefert:
              Code:
              public function __construct(FeedAggregator $feed) {
              	$this->feed = $feed;
              }
              
              public function index() {
              	/* ... */
              	$feed->instance()->get();
              }

              Mein momentaner Unit-Test sieht so aus:
              Code:
              <?php
              class DashboardControllerTest extends TestCase {
                  
                  public function setUp()
                  {
                      parent::setUp();
                      
                      Sentry::setUser(Sentry::findUserById(1));
                  }
                  
                  public function tearDown()
                  {
                      Sentry::logout();
                      Mockery::close();
                  }
                  
                  public function testIndex()
                  {
                      $this->userRepository    = Mockery::mock('UserRepository');
                      $this->projectRepository = Mockery::mock('ProjectRepository');
                      
                      $this->app->instance('UserRepository',    $this->userRepository);
                      $this->app->instance('ProjectRepository', $this->projectRepository);
                      
                      $this->projectRepository
                           ->shouldReceive('getProjectsWhereUserIsContributor')
                           ->once()
                           ->andReturn(new Illuminate\Database\Eloquent\Collection());
                      
                      $this->userRepository
                           ->shouldReceive('getCurrentlyOnline')
                           ->once()
                           ->andReturn(new Illuminate\Database\Eloquent\Collection());
                      
                      $response = $this->call('GET', '/');
                      
                      $this->assertResponseOk();
                      
                      $this->assertViewHas('feed');
                      $this->assertInstanceOf('Illuminate\Support\Collection', $response->original->getData()['feed']);
                      
                      $this->assertViewHas('user_projects');
                      $this->assertInstanceOf('Illuminate\Support\Collection', $response->original->getData()['user_projects']);
                      
                      $this->assertViewHas('currently_online');
                      $this->assertInstanceOf('Illuminate\Support\Collection', $response->original->getData()['currently_online']);
                  }
                  
              }
              Sollte so passen, oder? Kritik dran? Wie gesagt, ich schreibe selten Tests, und habe damit auch wenig Erfahrung - daher mein Beitrag.

              Was noch ergänzend gesagt sei: Test-Driven-Development klingt theoretisch ganz nett, praktisch bin ich wohl nicht in der Lage diese Schiene zu fahren. Momentan schreibe ich meine Tests paralell zur Implementierung.

              e/ Grad gemerkt: mein FA fehlt im Test. Nicht wundern, ist durch schlampiges Kopieren passiert... eigentlich gibts dafür auch noch n Mock-Objekt

              Kommentar


              • #8
                Ich hätte für jedes Dashboardwidget schon mal einen eigenen Controller erstellt. Ich verdrahte solche Controller dann direkt über das View-Template. Die Abhängigkeiten löse ich über einen (Quasi)DI-Container, so dass ich nicht gezwungen bin alle Abhängigkeiten vom Mastercontroller ins Dashbordtemplate zu geben. Aktuell hat dein Dashboard zu viele Zuständigkeiten. Zumal ich persönlich auch kein Fan von diesen gibMirAlleDatenDieSowohlEinGelbesAlsAuchEinRotesLi nksdrehendesDreieckHaben() Methoden bin. Builder á la ->whereXIs(...)->whereYIs(...)->getAll() sind flexibler.

                Wenn ich deinen Beitrag gerade noch mal lese, dann könnte ich dazu noch ne Menge schreiben. Aber mir fehlt da gerade leider die Zeit.

                Kommentar


                • #9
                  "Widgets" sind ja nicht im klassischen Sinne - sind ja lediglich die Daten, die im Widget dargestellt werden (sind alles nur Arrays). Ich habe auch Widgets, die ich einzeln lade. Der Ansatz aber klingt interessant - nur wie meinst "verdrahte solche Controller dann direkt über das View-Template"? Ich hätte ungern Logik in den Views.

                  Zu dem langen Methodennamen: ungünstiger Name, stimmt schon. Hieß ursprünglich nur "userProjects", was ich persönlich nicht mochte. Der neue Name aber ist zu lang. Ich bin selbst Fan von Method-Chaining, hier hätte es aber wenig Sinn - die Methode feuert nur ne Abfrage ab die relativ simpel ist. Mein FA beispielsweise ist da schon schon so aufgebaut (FA->forProject(17)->forUser(12)->get()...).

                  Kommentar


                  • #10
                    A) erstelle Deine Klasse so, dass sie von jedem nur ein Objekt braucht
                    Wenn das nicht möglich ist:
                    B) übergibt Deiner Klasse ein Compositum aus gleichartigen Objekten oder
                    C) leg Deine Klasse als Compositum gleichartiger Unterobjekte an, an die Du wieder nur ein Objekt übergebn musst (->A)
                    [COLOR="#F5F5FF"]--[/COLOR]
                    [COLOR="Gray"][SIZE="6"][FONT="Georgia"][B]^^ O.O[/B][/FONT] [/SIZE]
                    „Emoticons machen einen Beitrag etwas freundlicher. Deine wirken zwar fachlich richtig sein, aber meist ziemlich uninteressant.
                    [URL="http://www.php.de/javascript-ajax-und-mehr/107400-draggable-sorttable-setattribute.html#post788799"][B]Wenn man nur Text sieht, haben viele junge Entwickler keine interesse, diese stumpfen Texte zu lesen.“[/B][/URL][/COLOR]
                    [COLOR="#F5F5FF"]
                    --[/COLOR]

                    Kommentar


                    • #11
                      Zitat von Lokart Beitrag anzeigen
                      Ich hätte ungern Logik in den Views.
                      Keine Ahnung wer das mal schrub, dass Logik in Templates nichts zu suchen hat: In Templates kann man wunderbar ganz viel Logik verwenden.

                      Zum Beispiel ...
                      PHP-Code:
                      <?php if($user->active): ?>
                      <img src="green-status.png" />
                      <?php else: ?>
                      <img src="red-status.png" />
                      <?php endif ?>
                      ... ist Logik. Derartige Logik sollte es häufig in Templates geben.

                      Oder: Währungsformatierung, HTML-Formatierung, setzen und arbeiten mit Variablen.
                      Oder: Erstellung von Arrays aus Strings um damit eine Tabelle zu befüllen - auch ok.

                      Templates sollen Daten darstellen. Sie sollen die eingehenden Daten aber nicht verändern. Selbst koennen sie sich aber Hilfskonstrukte bauen, damit sie Daten anzeigen können. Ein Controller muss sie nur bereitstellen. Einem Controller sollte vielleicht nicht mal so wahnsinnig wichtig sein, ob die Daten in Form einer Tabelle oder eines Diagramms ausgegeben werden. Gerade Diagramme sind teilweise sehr eigen, wie die Daten strukturiert sein müssen.

                      An dem Punkt kann ich die Frage umkehren: Muss ein Controller wissen, wie ein Template die Daten darstellen will?

                      Kommentar


                      • #12
                        Keine Ahnung wer das mal schrub, dass Logik in Templates nichts zu suchen hat:
                        Hoffentlich war ich das nicht.
                        In Templates kann man wunderbar ganz viel Logik verwenden.
                        Muss man sogar. Sonst bleibt nämlich nur die Variante über verschachtelte Sub-Templates, was wiederum den aufrufenden Kontext zu Wissen über das Template zwingt.
                        [COLOR="#F5F5FF"]--[/COLOR]
                        [COLOR="Gray"][SIZE="6"][FONT="Georgia"][B]^^ O.O[/B][/FONT] [/SIZE]
                        „Emoticons machen einen Beitrag etwas freundlicher. Deine wirken zwar fachlich richtig sein, aber meist ziemlich uninteressant.
                        [URL="http://www.php.de/javascript-ajax-und-mehr/107400-draggable-sorttable-setattribute.html#post788799"][B]Wenn man nur Text sieht, haben viele junge Entwickler keine interesse, diese stumpfen Texte zu lesen.“[/B][/URL][/COLOR]
                        [COLOR="#F5F5FF"]
                        --[/COLOR]

                        Kommentar


                        • #13
                          "View"-Logik innerhalb der Templates ist durchaus ok meiner Meinung nach. Aber nicht Business-Logik. Controller (in Views) zusammenstöpseln zähle ich, sofern ichs richtig verstanden hab, eher zu Business-Logik.

                          Kommentar

                          Lädt...
                          X