3 Stimmen

Wie lässt sich effizienter Code durch Unit-Tests erstellen?

Ich nehme an einem TDD Coding Dojo teil, wo wir versuchen, reines TDD an einfachen Problemen zu üben. Dabei ist mir aufgefallen, dass der Code, der aus den Unit-Tests hervorgeht, nicht der effizienteste ist. Das ist in den meisten Fällen in Ordnung, aber was ist, wenn der Codeverbrauch so wächst, dass die Effizienz zu einem Problem wird?

Ich liebe die Art und Weise der Code entsteht aus Unit-Tests, aber ist es möglich, die Effizienz Eigenschaft entstehen durch weitere Tests?

Hier ist ein triviales Beispiel in Ruby: Primfaktorzerlegung. Ich habe einen reinen TDD-Ansatz verfolgt, bei dem die Tests einen nach dem anderen bestanden haben und meinen ursprünglichen Akzeptanztest (unten kommentiert) validiert haben. Welche weiteren Schritte könnte ich unternehmen, wenn ich einen der Tests generische Algorithmen zur Primfaktorzerlegung auftauchen? Um den Problembereich einzugrenzen, sagen wir, ich möchte eine quadratisches Sieb Umsetzung ... In diesem konkreten Fall kenne ich den "optimalen Algorithmus", aber in den meisten Fällen wird der Kunde einfach eine Anforderung hinzufügen, dass die Funktion in einer bestimmten Umgebung in weniger als "x" Zeit läuft.

require 'shoulda'
require 'lib/prime'

class MathTest < Test::Unit::TestCase
  context "The math module" do
    should "have a method to get primes" do 
      assert Math.respond_to? 'primes'
    end
  end
  context "The primes method of Math" do
    should "return [] for 0" do
      assert_equal [], Math.primes(0)
    end
    should "return [1] for 1 " do
      assert_equal [1], Math.primes(1)
    end
    should "return [1,2] for 2" do 
      assert_equal [1,2], Math.primes(2)
    end
    should "return [1,3] for 3" do 
      assert_equal [1,3], Math.primes(3)
    end
    should "return [1,2] for 4" do 
      assert_equal [1,2,2], Math.primes(4)
    end 
    should "return [1,5] for 5" do 
      assert_equal [1,5], Math.primes(5)
    end   
    should "return [1,2,3] for 6" do 
      assert_equal [1,2,3], Math.primes(6)
    end       
    should "return [1,3] for 9" do 
      assert_equal [1,3,3], Math.primes(9)
    end        
    should "return [1,2,5] for 10" do 
      assert_equal [1,2,5], Math.primes(10)
    end                  
  end
#  context "Functionnal Acceptance test 1" do
#    context "the prime factors of 14101980 are 1,2,2,3,5,61,3853"do      
#      should "return  [1,2,3,5,61,3853] for ${14101980*14101980}" do
#        assert_equal [1,2,2,3,5,61,3853], Math.primes(14101980*14101980)
#      end
#    end
#  end
end

und der naive Algorithmus, den ich mit diesem Ansatz entwickelt habe

module Math
  def self.primes(n)
    if n==0
      return []
    else
      primes=[1]  
      for i in 2..n do
        if n%i==0          
          while(n%i==0)
            primes<<i
            n=n/i
          end
        end
      end      
      primes
    end
  end
end

bearbeiten 1 Nach den ersten Antworten zu urteilen, habe ich mich in meiner ursprünglichen Beschreibung wohl nicht klar ausgedrückt: Der Leistungstest ist nicht ein Standardbestandteil meines Einheitstests, es ist ein neue Abnahmeprüfung geschrieben als Antwort auf eine spezielle Anforderung vom Kunden.

bearbeiten 2 Ich weiß, wie man die Ausführungszeit testet, aber es scheint, dass der Übergang vom trivialen Algorithmus zum optimierten Algorithmus ein großer Schritt ist. auftauchen Oder anders ausgedrückt: Wie zerlegt man die Migration vom trivialen Code zum optimalen Code? Einige erwähnten, dass es sich um einen problemspezifischen Ansatz handelt: Ich habe ein Beispielproblem angegeben, bei dem ich nicht weiß, wie es weitergehen soll.

4voto

kriss Punkte 22473
  • TDD ist für KorrektheitNichtregression und konzentrieren sich auf Unit-Tests
  • Profilierung ist für Leistung und es ist ein Funktionsprüfung Problem.

Ich habe auch an einem wöchentlichen TDD-Coding-Dojo teilgenommen, und wir haben einige Experimente durchgeführt, um zu sehen, ob es möglich ist, es für algorithmische Zwecke (einen besseren Algorithmus finden, einen Algorithmus finden, für den es keinen offensichtlichen gibt) oder eingebaute Leistungsbeschränkungen zu verwenden.

Bei der Verwendung von TDD in Dojo versuchen wir, die folgenden Regeln zu befolgen

  • den einfachsten Test schreiben, der den bestehenden Code bricht (oder ein Bier bezahlen, wenn er den Code nicht bricht)
  • den einfachsten Code schreiben, der den Test bestehen lässt
  • Refaktorierung des Codes (unter Verwendung von Code-Smells) vor dem Hinzufügen eines Tests
  • Refaktorieren Sie auch Tests, bevor Sie neue Tests hinzufügen.

Angesichts dieser Regeln haben wir mehr Spielraum für Experimente, als es auf den ersten Blick scheint. Wir können die Definition des Begriffs "am einfachsten" abändern und Codegerüche hinzufügen, um die Effizienz zu berücksichtigen (grundsätzlich gilt: Wenn uns mehrere einfache Möglichkeiten zur Implementierung einer Sache einfallen, bevorzugen wir die effizienteste, und wenn wir einen effizienteren - aber immer noch einfachen oder bekannten - Algorithmus kennen als den in unserem Code verwendeten, ist das ein Geruch).

Zusammenfassend war das Ergebnis, dass TDD selbst nicht gut geeignet ist, um die Gesamtleistung des Codes vorherzusagen und von Anfang an effizienten Code zu erreichen, auch wenn es uns mit TDD und Refactoring gelang, einen besseren Einblick in unseren Code zu bekommen und ihn zu verbessern, um eine bessere Lesbarkeit zu erreichen einige offensichtliche Leistungsengpässe zu vermeiden. Der Versuch, auf dieser Testebene Leistungseinschränkungen in den Code einzufügen, war in der Regel katastrophal (wir bekamen viel zu komplexen Code und Tests, die oft fehlerhaft oder zu komplex waren, um sie zu ändern).

Ein Grund dafür ist, dass TDD arbeiten wir in der Regel mit sehr kleinen Tests gesetzt (die einfachsten Tests, die fehlschlagen). Andererseits treten bei realen Datensätzen mehr Leistungsprobleme auf, die sehr schlecht mit den oben genannten Regeln zusammenpassen. Leistungstests, auch wenn sie formal immer noch Unit-Tests sind, ähneln eher funktionalen Tests. Übliche Optimierungsstrategien beinhalten das Hinzufügen von Caches oder die Berücksichtigung einiger Eigenschaften der realen Datenverteilung oder das Negieren von Änderungen in den User Stories, wenn ein kleines Vorteilsmerkmal große negative Auswirkungen auf die Leistung hat. All dies kann nicht wirklich in TDD eingebaut werden, sondern wird eher beim Profiling von Code gefunden.

Ich glaube Leistungen Ziel sind im Grunde ein Funktionsprüfung Problem.

3voto

Nein, und Sie sollten es auch nicht versuchen. Unit-Tests testen die Korrektheit, nicht die Effizienz - sie zu zwingen, die Effizienz zu testen, ist eine Form der verfrühten Optimierung.

3voto

Gishu Punkte 130442

TDD kann Ihnen nicht bei der Ableitung von Algorithmen helfen - wenn das Ihre Frage ist. Es ist einer jener Nischenbereiche, die sich nicht auf die Stärken von TDD (Nische: im Vergleich zu den Horden Churning aus Unternehmenssoftware, die in eine Zillion Frameworks / Bibliotheken ruft) verleihen.

Nichts hindert Sie daran, weiterhin TDD anzuwenden - Sie können einen Leistungstest schreiben, aber was wäre Ihre Zielvorgabe? Was ist, wenn es eine Möglichkeit gibt, Ihre Spezifikation zu halbieren? Diese Fragen können nicht beantwortet werden, ohne Ihren Code zu profilieren und ihn auf die richtige Art und Weise zu bearbeiten. Testgetriebenes Vorgehen wird diese Fragen nicht beantworten; es würde Ihnen höchstens ein Sicherheitsnetz geben, um zu erkennen, ob Sie gerade bestehenden Code kaputt gemacht haben.

Sie könnten z. B. TDD durchführen, um eine Sortierung zu implementieren, aber die Chancen, einen neuen Algorithmus zu finden oder einen bestehenden effizienten Algorithmus wie Quicksort zu erreichen, sind gering. Es sei denn, Sie kennen die Grundsätze des Algorithmusentwurfs und arbeiten bewusst darauf hin.

Update: Unterstützende Beweise. http://www.markhneedham.com/blog/2009/12/10/tdd-big-leaps-and-small-steps/ Es gibt noch ein paar mehr - aber die sind wie Diskussionen auf reddit: Meinungsäußerungen ohne Unterstützung. Ich poste sie nicht.

2voto

Ira Baxter Punkte 91118

Bei Unit-Tests wird im Allgemeinen die Korrektheit der Funktionalität stückweise überprüft.

Sie können sicherlich Zeitbeschränkungen zu einem Einheitstest hinzufügen, aber es kann schwierig sein, sie auf technologieunabhängige Weise auszudrücken (z. B. O(N ln N)).

Und nur weil Sie einen Unit-Test schreiben können, der darauf besteht, dass ein Ergebnis in konstanter Zeit geliefert wird, bedeutet das nicht, dass der Programmierer für die Unit unbedingt einen Algorithmus finden kann, der diesen Effekt erzielt. (Natürlich kann es sein, dass ihm auch nicht einfällt, wie man die Funktionalität korrekt implementiert.

Wenn Sie dies tun, würde ich vorschlagen, die Funktionstests und die Leistungstests zu trennen. Dann können Sie zumindest feststellen, ob der Code funktioniert, bevor Sie entscheiden, dass die Leistung wirklich schlecht ist.

1voto

JasDev Punkte 716

In Ihrem Unit-Test-Code können Sie Code hinzufügen, der die verstrichene Zeit des Zielcodes misst. Der Pseudocode würde etwa so aussehen:

start_time = date_time_now();
Math.primes(1000);
stop_time = date_time_now();
assert stop_time-start_time < target_execution_time;

Einige Textbausteine enthalten bereits die verstrichene Zeit, auf die Sie sich beziehen können. Dies macht zusätzlichen Boilerplate-Code für die Zeitmessung überflüssig.

Außerdem ist elapsed_time nur ein Beispiel für eine zu verwendende "Effizienz"-Metrik. Andere zu prüfende Metriken sind z. B. CPU-Zeit, Durchsatz, übertragene Ein-/Ausgabebytes usw.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X