Themenspringer http://tammofreese.de/ Im Themenspringer schreibt Tammo Freese über verschiedenste Themen, unter anderem Agilität, Ruby on Rails, Eclipse, Mac OS X und XHTML/CSS. de-de Copyright 2005-2009 Tammo Freese business@tammofreese.de business@tammofreese.de Thu, 03 Sep 2009 12:52:00 +0100 Thu, 03 Sep 2009 12:52:00 +0100 http://blogs.law.harvard.edu/tech/rss Goodbye Rails, Hello OS X (iPhone/Mac) http://tammofreese.de/2009/09/03/goodbye-rails-hello-os-x-iphone-mac

Ich habe mich vor einigen Monaten entschlossen, mit Ruby on Rails aufzuhören, um mich in Zukunft auf technischer Seite voll auf Beratung, Entwicklung und Schulung zum OS X-Betriebssystem (sowohl iPhone als auch Mac) zu konzentrieren. Aus meinem Netzwerk kamen Fragen, warum ich diesen Wechsel mache. Meine Antworten darauf habe ich hier zusammengefasst.

Warum iPhone/Mac?

Anfang 2003 wechselte ich auf Mac OS X (damals 10.2 auf einem G3-iBook), weil mich das Gesamtkonzept überzeugte: Das Design der Rechner, die Kombination von Software und Hardware, ein BSD-basiertes Unix-Betriebssystem, vor allem aber die Entwicklung mit Cocoa und Objective-C. Damals gab es aber keine für mich wahrnehmbare Nachfrage nach Mac-Entwicklung, auch in meinem Netzwerk war ich mit dem Mac eher ein Exot. Daher verfolgte ich die Entwicklung für den Mac nur auf Sparflamme weiter. Meinen Switch habe ich aber nie bereut, im Gegenteil.

Mit dem Wechsel auf Intel-Macs wurde das allgemeine Interesse an Mac OS X deutlich größer, und mit dem iPhone SDK sah ich die Chance, endlich für die Plattform meiner Wahl zu entwickeln. Mac und iPhone sind meiner Meinung nach mit die interessantesten Entwicklungsplattformen: Die Sprache ist OK und Bibliotheken und Werkzeuge sind großartig, und durch die ständige, inkrementelle Weiterentwicklung seitens Apple gibt es auch immer etwas Neues zu entdecken.

Warum kein Rails mehr?

Rails ist ein hervorragendes Framework für die Entwicklung von dynamischen Webseiten. Mein Interesse geht jedoch mittlerweile wieder mehr in Richtung hin zu nativen Applikationen. Die einzigen Webapplikationen, für die ich mich noch begeistern kann, versuchen im Browser möglichst nah an native Applikationen heranzukommen, wie zum Beispiel 280slides (implementiert mit Cappuccino) und Mobile Me (implementiert mit SproutCore). Für solche Applikationen ist Rails aber bestenfalls als Backend interessant.

Warum nicht beides?

Ich kann nicht alles machen. Als ich 2005 auf Ruby on Rails umgeschwenkt bin, habe ich Java/Eclipse aus meinem Portfolio genommen, mit meinem technischen Wechsel auf OS X (iPhone/Mac) nehme ich Ruby on Rails aus meinem Portfolio. Meiner Meinung nach ist es besser, sich voll auf ein technisches Thema zu konzentrieren. Zudem möchte ich auch endlich mal meine Dissertation fertigstellen, und in zwei technischen Themen auf dem neusten Stand zu bleiben, würde mir dafür noch weniger Zeit lassen.

Warum nicht auch Blackberry, Android oder Palm Pre?

Wie oben schon gesagt: Ich kann nicht alles machen. Blackberry, Android und Palm Pre sind Plattformen mit anderen Programmiersprachen, anderen Frameworks, anderen Werkzeugen, anderen Bedienkonzepten – also fundamental verschiedene Umgebungen im Mobile-Bereich, so wie Windows, Mac OS X und Linux fundamental verschiedene Umgebungen im Desktop-Bereich sind.

Was ist mit Ruby?

Ich verwende zwar weiter Ruby, biete aber keine Beratung mehr dazu an. In 2000 bin durch Frank Westphal und das Pickaxe-Buch der Pragmatic Programmer auf Ruby gestoßen, und Ruby ist bis heute eine meiner Lieblingssprachen.

]]>
Thu, 03 Sep 2009 12:52:00 +0100 http://tammofreese.de/2009/09/03/goodbye-rails-hello-os-x-iphone-mac
Tuning the Performance of the Ruby Builder Library http://tammofreese.de/2009/09/02/tuning-the-performance-of-the-ruby-builder-library

In a previous article, I have shown how to convert HanDeDict (a free chinese-german dictionary) for using it in Apple's Dictionary. The main part was the generation of a big XML file using the Builder Library.

Generating the XML file now takes more than 4 minutes, almost all of the time spent in the XML generation. In this article, I show how to improve the performance of the Builder Library. Generating the XML file got up to 36% faster. If you would like to get the code, you may clone it from GitHub.

Finding the Bottleneck

To test for performance, we use five scenarios. The first one is the XML generation for HanDeDict. In the second one, we generate an in-memory xml string with one million tags, each containing a short text:

require 'builder/xmlmarkup'
require 'benchmark'

result = Benchmark.measure do
  xm = Builder::XmlMarkup.new
  xm.instruct!
  xm.links do
    1000000.times do |i|
      xm.link "Item #{i.to_s}"
    end
  end
  x = xm.target!
end
puts result

The third example is test/performance.rb from the Builder library itself. It renders XML with very few tags, but a lot of text:

#!/usr/bin/env ruby

require 'builder/xmlmarkup'
require 'benchmark'

text = "This is a test of the new xml markup. Iñtërnâtiônàlizætiøn" * 10000

include Benchmark          # we need the CAPTION and FMTSTR constants
include Builder
n = 50
Benchmark.benchmark do |bm|
  tf = bm.report("base")   {
    n.times do
      x = XmlMarkup.new
      x.text(text)
      x.target!
    end
  }
  def XmlMarkup._escape(text)
    text.to_xs
  end
  tf = bm.report("to_xs")   {
    n.times do
      x = XmlMarkup.new
      x.text(text)
      x.target!
    end
  }
end

It should be noted here that performance.rb actually tests a special case: The term Iñtërnâtiônàlizætiøn is in CP1252 (Windows) encoding, for which Builder provides a workaround. So as fourth scenario, we use the same file saved as UTF8. In the fifth and last scenario, we replace Iñtërnâtiônàlizætiøn with Internationalization to test ASCII performance.

The initial results are:

Example 1:      251.330000   1.280000 252.610000 (256.420148)
Example 2:       48.910000   0.240000  49.150000 ( 49.680870)
Example 3:  base 97.400000   0.760000  98.160000 ( 98.609859)
           to_xs 98.230000   0.730000  98.960000 ( 99.363800)
Example 4:  base 98.250000   0.790000  99.040000 ( 99.441164)
           to_xs 99.100000   0.760000  99.860000 (100.257412)
Example 5:  base 88.380000   0.540000  88.920000 ( 89.046175)
           to_xs 88.240000   0.630000  88.870000 ( 89.225532)

To get some hints where the time is spent, we install the ruby-perf gem and change a copy of the second scenario to print out a report.

require 'builder'
require 'rubygems'
require 'ruby-prof'

result = RubyProf.profile do
  xm = Builder::XmlMarkup.new
  xm.instruct!
  xm.links do
    100000.times do |i|
      xm.link i.to_s
    end
  end
  x = xm.target!
end
RubyProf::FlatPrinter.new(result).print(STDOUT, 0)

Here are the first lines of the report. It mainly consists of a table listing how much time was spent in each method, and how much in child (read: called) methods. Note that due to the profiling, the scenario runs a lot slower, so we only write 100,000 nodes.

Thread ID: 136060
Total: 62.546996

 %self     total     self     wait    child    calls  name
at top level in 24.01 37.94 15.02 0.00 22.92 488898 Fixnum#xchr (/Users/tammofreese/Documents/builder/trunk/test/../lib/builder/xchar.rb at line 95
at top level in 9.73 9.18 6.08 0.00 3.10 1466694 Kernel#=== (ruby_runtime at line 0
at top level in 8.07 61.54 5.05 0.00 56.50 100000 Builder::XmlBase#method_missing-1 (/Users/tammofreese/Documents/builder/trunk/test/../lib/builder/xmlbase.rb at line 40
at top level in 6.60 6.24 4.13 0.00 2.11 977803 Hash#[] (ruby_runtime at line 0
at top level in 5.65 3.53 3.53 0.00 0.00 1666700 Fixnum#== (ruby_runtime at line 0
at top level in 5.06 5.23 3.16 0.00 2.06 488898 Range#=== (ruby_runtime at line 0
at top level in 4.13 40.52 2.58 0.00 37.94 100002 Array#map (ruby_runtime at line 0
at top level in 3.48 3.94 2.18 0.00 1.76 100001 Builder::XmlMarkup#_start_tag (/Users/tammofreese/Documents/builder/trunk/test/../lib/builder/xmlmarkup.rb at line 290
at top level in 3.38 2.11 2.11 0.00 0.00 977800 Hash#default (ruby_runtime at line 0
at top level in 3.30 2.06 2.06 0.00 0.00 977796 Fixnum#<=> (ruby_runtime at line 0

As we see, most of the time is spent in the Fixnum#xchr method, which is called almost half a million times. So we target this method for performance improvement. What's more, we see a huge number of calls to Kernel#===, Hash#[], Fixnum#==, and Range#===.

Improving the Performance at the Bottleneck

Let's have a look at the Fixnum#xchr method:
  def xchr(escape=true)
    n = XChar::CP1252[self] || self
    case n when *XChar::VALID
      XChar::PREDEFINED[n] or (n<128 ? n.chr : (escape ? "&##{n};" : [n].pack('U*')))
    else
      '*'
    end
  end

It contains a fix for CP1252-encoded files from the windows world, then it decides whether n is a valid unicode code point. Invalid numbers are mapped to a string containing an asterisk. Valid numbers are mapped to strings containing the respective UTF-8 characters. Characters outside the ASCII range are optionally escaped.

The case n when *XChar::VALID may be the source of all the case equality operator and Fixnum#== calls. Let's look at the definition of VALID:

  VALID = [
    0x9, 0xA, 0xD,
    (0x20..0xD7FF), 
    (0xE000..0xFFFD),
    (0x10000..0x10FFFF)
  ]

The first three elements of the array are the unicode codepoints of tab, linefeed and carriage return. Then, three ranges are used that contain a lot of unicode code points. A typical text will contain many characters in the first two ranges, and not many tabs, linefeeds, carriage returns, or characters from the third range (the unicode codepoints above 0xFFFF are not that often used). We change the array so that the common cases are checked first:

  VALID = [
    (0x20..0xD7FF), 
    (0xE000..0xFFFD),
    0x9, 0xA, 0xD,
    (0x10000..0x10FFFF)
  ]

After this change, the performance improves by about 3-16%:

Example 1:      244.080000   1.250000 245.330000 (249.456791)
Example 2:       43.220000   0.150000  43.370000 ( 43.529551)
Example 3:  base 81.560000   0.560000  82.120000 ( 82.182873)
           to_xs 82.370000   0.510000  82.880000 ( 82.938198)
Example 4:  base 81.750000   0.670000  82.420000 ( 82.601055)
           to_xs 82.400000   0.620000  83.020000 ( 83.276765)
Example 5:  base 74.350000   0.620000  74.970000 ( 75.106275)
           to_xs 74.410000   0.530000  74.940000 ( 75.092212)

The next change is to cache all three constants CP1252, VALID and PREDEFINED directly in Fixnum:

VALID = Builder::XChar::VALID if ! defined?(VALID)
PREDEFINED = Builder::XChar::PREDEFINED if ! defined?(PREDEFINED)
CP1252 = Builder::XChar::CP1252 if ! defined?(CP1252)

def xchr(escape=true)
  n = CP1252[self] || self
  case n when *VALID
    PREDEFINED[n] or (n<128 ? n.chr : (escape ? "&##{n};" : [n].pack('U*')))
  else
    '*'
  end
end

Now the scenarios run faster by about 5-23%:

Example 1:      237.950000   1.200000 239.150000 (243.143989)
Example 2:       41.710000   0.260000  41.970000 ( 46.755385)
Example 3:  base 76.360000   0.670000  77.030000 ( 77.289001)
           to_xs 77.210000   0.600000  77.810000 ( 78.047175)
Example 4:  base 75.260000   0.760000  76.020000 ( 76.714416)
           to_xs 76.090000   0.670000  76.760000 ( 77.237976)
Example 5:  base 67.790000   0.690000  68.480000 ( 68.834157)
           to_xs 67.740000   0.510000  68.250000 ( 68.404581)

Then, we eliminate the local variable n:

def xchr(escape=true)
  return CP1252[self].xchr(escape) if CP1252.key?(self)
  case self when *VALID
    PREDEFINED[self] or (self<128 ? self.chr : (escape ? "&##{self};" : [self].pack('U*')))
  else
    '*'
  end
end

Again, the scenarios run faster. We have now improvements of about 6-29% compared to the original implementation:

Example 1:      234.610000   1.350000 235.960000 (240.837129)
Example 2:       40.040000   0.180000  40.220000 ( 48.501536)
Example 3:  base 71.440000   0.670000  72.110000 ( 72.325842)
           to_xs 72.300000   0.600000  72.900000 ( 73.041512)
Example 4:  base 70.460000   0.700000  71.160000 ( 71.500979)
           to_xs 71.080000   0.570000  71.650000 ( 71.827479)
Example 5:  base 62.980000   0.630000  63.610000 ( 63.944886)
           to_xs 62.910000   0.530000  63.440000 ( 63.588532)

Intermezzo: Fixing two Bugs along the Way

In Fixnum#xchr, the CP1252 fix is always applied. It maps CP1252 codes to their unicode counterparts:

CP1252 = {        # :nodoc:
  128 => 8364,    # euro sign
  130 => 8218,    # single low-9 quotation mark
  131 =>  402,    # latin small letter f with hook
  132 => 8222,    # double low-9 quotation mark
  133 => 8230,    # horizontal ellipsis
  134 => 8224,    # dagger
  135 => 8225,    # double dagger
  136 =>  710,    # modifier letter circumflex accent
  137 => 8240,    # per mille sign
  138 =>  352,    # latin capital letter s with caron
  139 => 8249,    # single left-pointing angle quotation mark
  140 =>  338,    # latin capital ligature oe
  142 =>  381,    # latin capital letter z with caron
  145 => 8216,    # left single quotation mark
  146 => 8217,    # right single quotation mark
  147 => 8220,    # left double quotation mark
  148 => 8221,    # right double quotation mark
  149 => 8226,    # bullet
  150 => 8211,    # en dash
  151 => 8212,    # em dash
  152 =>  732,    # small tilde
  153 => 8482,    # trade mark sign
  154 =>  353,    # latin small letter s with caron
  155 => 8250,    # single right-pointing angle quotation mark
  156 =>  339,    # latin small ligature oe
  158 =>  382,    # latin small letter z with caron
  159 =>  376,    # latin capital letter y with diaeresis
}

But this does not always make sense. As an example, let us look at the number 134. A string containing only this number in one byte should be regarded as CP1252-encoded as it is invalid UTF-8. Therefore it should be mapped to the corresponding unicode code point 8224. However, a string that contains the UTF-8 bytes for codepoint 134 must not be changed. We add a test to check whether this is a problem in the current implementation:

def test_do_not_fix_utf8_as_win_1252
  assert_equal '&#8224;', "\x86".to_xs         # CP1252 dagger
  assert_equal '&#134;',  "\xC2\x86".to_xs     # UTF-8 reverse line feed
end

Turns out it is a problem: the test fails. The method String#to_xs which calls Fixnum#xchr looks like this:

def to_xs(escape=true)
  unpack('U*').map {|n| n.xchr(escape)}.join # ASCII, UTF-8
rescue
  unpack('C*').map {|n| n.xchr}.join # ISO-8859-1, WIN-1252
end

CP1252 encoding does only make sense if the UTF-8 unpack fails. So we change the code to

CP1252 = Builder::XChar::CP1252 if ! defined?(CP1252)

def to_xs(escape=true)
  unpack('U*').map {|n| n.xchr(escape)}.join # ASCII, UTF-8
rescue
  unpack('C*').map {|n| (CP1252[n] || n).xchr}.join # ISO-8859-1, WIN-1252
end

Then, we delete the CP1252 constant and the first line of Fixnum#xchar:

class Fixnum
  VALID = Builder::XChar::VALID if ! defined?(VALID)
  PREDEFINED = Builder::XChar::PREDEFINED if ! defined?(PREDEFINED)

  def xchr(escape=true)
    case self when *VALID
      PREDEFINED[self] or (self<128 ? self.chr : (escape ? "&##{self};" : [self].pack('U*')))
    else
      '*'
    end
  end
end

Now the test passes. As the CP1252 fix is not applied for UTF-8 anymore, we get quite a performance boost. Our version now needs 10-37% less time than the original implementation:

Example 1:      225.130000   1.230000 226.360000 (229.601577)
Example 2:       36.010000   0.110000  36.120000 ( 36.246593)
Example 3:  base 76.140000   0.680000  76.820000 ( 77.060833)
           to_xs 77.080000   0.630000  77.710000 ( 77.986746)
Example 4:  base 62.260000   0.580000  62.840000 ( 63.280986)
           to_xs 62.710000   0.560000  63.270000 ( 63.408446)
Example 5:  base 55.040000   0.620000  55.660000 ( 55.865656)
           to_xs 54.780000   0.510000  55.290000 ( 55.367186)

Another bug we have spotted in String#to_xs is that escaping is ignored if the CP1252 fix is applied. We add a test to confirm that:

def test_win_1252_escaping
  assert_equal '&#8364;', "\x80".to_xs(true)   # euro with escaping
  assert_equal '€', "\x80".to_xs(false)        # euro without escaping
end

The test fails as expected. Now we pass the escape parameter to Fixnum#to_xs:

def to_xs(escape=true)
  unpack('U*').map {|n| n.xchr(escape)}.join # ASCII, UTF-8
rescue
  unpack('C*').map {|n| (CP1252[n] || n).xchr(escape)}.join # ISO-8859-1, WIN-1252
end

Avoiding the Bottleneck

Here is what is left of Fixnum#xchr:

def xchr(escape=true)
  case self when *VALID
    PREDEFINED[self] or (self<128 ? self.chr : (escape ? "&##{self};" : [self].pack('U*')))
  else
    '*'
  end
end

So far, we tried to make Fixnum#xchr faster. But maybe it is possible to do move the whole Fixnum#xchr functionality directly inside the String#to_xs method? We would not need a method call per character anymore, which should give us a huge performance boost. So what has to be done in String#to_xs?

  1. Apply the CP1252 fix if needed.
  2. Replace invalid characters with '*'.
  3. Replace the characters for the code points in PREDEFINED with the mapped strings.
  4. Escape if needed.

Here is the new implementation:

def to_xs(escape=true)
  result = begin
    unpack('U*')
    dup
  rescue
    unpack('C*').map! {|n| CP1252[n] || n }.pack("U*")
  end
  result.gsub!(INVALID_UTF8_MATCHER, "*")
  result.gsub!(PREDEFINED_UTF_MATCHER) { |match| PREDEFINED[match] }
  result.gsub!(NON_ASCII_UTF8_MATCHER) { |match| "&##{match.unpack('U').first};"} if escape
  result
end

What we still need to do is to define the constants. First, we define a helper lambda in String that converts a given Integer or Range to a character class entry. As an example, 0x41 will be converted to "A" and (0x41..0x5A) to "A-Z":

to_character_class_entry = lambda do |entry|
  next "#{[entry].pack('U')}" if entry.is_a? Integer
  next "#{[entry.first].pack('U')}-#{[entry.last].pack('U')}" if entry.is_a? Range
end 

Using this helper, we define INVALID_UTF8_MATCHER:

INVALID_UTF8_MATCHER = /[^#{Builder::XChar::VALID.map(&to_character_class_entry).join}]/u

For NON_ASCII_UTF8_MATCHER, we need all the values from VALID which are greater than 0x7F. To get those, we roll back VALID and split the first range:

  VALID = [
    0x9, 0xA, 0xD,
    (0x20..0x7F), 
    (0x80..0xD7FF), 
    (0xE000..0xFFFD),
    (0x10000..0x10FFFF)
  ]

Now we can define NON_ASCII_UTF8_MATCHER:

NON_ASCII_UTF8_MATCHER = /[#{Builder::XChar::VALID[4..-1].map(&to_character_class_entry).join}]/u

The PREDEFINED that we need in String#to_xs is like the one in Builder::XChar, except that it needs Strings instead of Fixnums as keys:

PREDEFINED = Builder::XChar::PREDEFINED.inject({}) { |sum, (key, val)| sum[[key].pack('U')] = val; sum }

The PREDEFINED_UTF8_MATCHER constant references a regular expression that matches all the characters with unicode codepoints contained in Builder::XChar::PREDEFINED.keys:

PREDEFINED_UTF_MATCHER = /[#{Builder::XChar::PREDEFINED.keys.map(&to_character_class_entry).join}]/u

Results

Here are the performance results for the bootleneck-avoiding version of String#to_xs:

Example 1:      159.390000   0.970000 160.360000 (163.721333)
Example 2:       17.000000   0.080000  17.080000 ( 17.265984)
Example 3:  base 47.410000   0.470000  47.880000 ( 48.071752)
           to_xs 47.390000   0.470000  47.860000 ( 47.995763)
Example 4:  base 13.910000   0.350000  14.260000 ( 14.326577)
           to_xs 14.510000   0.360000  14.870000 ( 14.921875)
Example 5:  base  1.550000   0.230000   1.780000 (  1.788842)
           to_xs  1.470000   0.230000   1.700000 (  1.703043)

The final version needs 36-98% less time than the original version (ruby1.8.6p114). The last number is somewhat misleading, as the corresonding test tests the to_xs performance for huge ASCII texts, which may not be the typical case. However, even for the second example, which builds a structure with small nodes, the execution is almost three times as fast as before.

Patches for the bug fixes and the performance fixes were sent to the maintainers of the builder library, unfortunately not much of it has shown up in their repository. So if you would like to use the improved version, grab a copy of the source from GitHub.

]]>
Wed, 02 Sep 2009 21:00:00 +0100 http://tammofreese.de/2009/09/02/tuning-the-performance-of-the-ruby-builder-library
Das Stomp-Protokoll, Telnet und ASCII NUL http://tammofreese.de/2008/08/14/stomp-protokoll-telnet-und-ascii-nul

Das Stomp-Protokoll gibt ein Format vor, mit sehr einfach Clients für Message Broker wie ActiveMQ geschrieben werden können.

Um mit dem Stomp-Protokoll zu Testzwecken auf einen ActiveMQ-Server zuzugreifen, kann telnet verwendet werden:

telnet localhost 61613

Um nun eine Verbindung gemäß dem Stomp-Protkoll aufzubauen, soll folgendes eingegeben werden:

CONNECT
login:john
passcode:doe

^@

Hier bekommt man aber ein Problem, zumindest mit einer deutschen Tastatur und einem Mac: Am Ende soll ^@ eingegeben werden, also die Control-Taste und das @-Zeichen, um ein ASCII NUL-Steuerzeichen zu erzeugen. Bei der amerikanischen Tastaturbelegung funktioniert dies: ^@ ist dort ctrl-shift-2. Auf der deutschen Tastatur wäre ^@ der Tastendruck ctrl-alt-L, der aber leider kein ASCII NUL erzeugt. Auch die Tastenkombination der englischen Tastatur hilft bei der deutschen Belegung nicht weiter.

Nach einiger Zeit der Suche fand ich eine Lösung: Sowohl auf der deutschen als auch auf der englischen Tastaturbelegung kann man alternativ ctrl-space eingeben, um das ASCII NUL-Steuerzeichen einzugeben. Es wird dann tatsächlich auch als ^@ dargestellt.

]]>
Thu, 14 Aug 2008 20:00:00 +0100 http://tammofreese.de/2008/08/14/stomp-protokoll-telnet-und-ascii-nul
HanDeDict für Apple Dictionary http://tammofreese.de/2008/03/22/handedict-fuer-apple-dictionary

Seit Mac OS X 10.5 Leopard kann man mit wenig Aufwand selbst Lexika erstellen, die dann genauso verwendet werden können wie die von Apple mitgelieferten.

Dieser Artikel zeigt, wie ein bestehendes Lexikons für Leopard konvertiert wird. Als Beispiel konvertieren wir das chinesisch-deutsche Lexikon HanDeDict. Der eigentliche Konvertierungsteil wird in Ruby implementiert.

Diejenigen, die lediglich das Lexikon verwenden wollen, können es hier herunterladen: HanDeDict.dictionary.zip (33,8 MB). (Update 20.04.2008:: Das jeweils aktuelle Download steht nun auf der HanDeDict-Downloadseite zur Verfügung!) Wer sich für die Erzeugung von Lexika für Apple Dictionary interessiert, sollte weiterlesen. :)

Projektsetup

Ausgangsbasis ist Mac OS X 10.5 mit installierten Development Tools. Zuerst kopieren wir uns das Projekt-Template aus dem Dictionary Development Kit:

cp /Developer/Extras/Dictionary\ Development\ Kit/project_templates/ ~/Desktop/HanDeDict

Von dem Inhalt des Verzeichnisses ~/Desktop/HanDeDict benötigen wir nur vier Dateien:

MyDictionary.xml
Diese Datei gibt ein Beispiel dafür, wie das XML-Format aussieht, aus dem ein Lexikon erstellt werden kann. Eine solche XML-Datei erzeugen wir später aus dem HanDeDict. Um das Projektsetup zu prüfen, lassen wir sie erst einmal bestehen. Allerdings geben wir ihr den Namen HanDeDict.xml.
MyDictionary.css
Für die Formatierung der Einträge im Lexikon wird CSS verwendet. Wir benennen diese Datei in HanDeDict.css um.
MyInfo.plist
In der Property List werden verschiedene Attribute unseres Lexikons beschrieben. Wir benennen diese Datei in HanDeDict.plist um.
Makefile
Das Makefile, mit dem wir unser Dictionary erstellen können. Innerhalb der Datei ändern wir die Dateinamen zu unseren Dateinamen, und geben den Namen unseres Lexikons an. Dazu ersetzen wir
DICT_NAME		=	"My Dictionary"
DICT_SRC_PATH		=	MyDictionary.xml
CSS_PATH		=	MyDictionary.css
PLIST_PATH		=	MyInfo.plist
durch
DICT_NAME		=	"HanDeDict"
DICT_SRC_PATH		=	HanDeDict.xml
CSS_PATH		=	HanDeDict.css
PLIST_PATH		=	HanDeDict.plist

Nun können wir das Lexikon testweise erstellen und installieren. Dazu verwenden wir make im Terminal. Mittels make install kopieren wir das erstellte Lexikon nach ~/Library/Dictionaries:

make && make install

In den Beispieldaten gibt es den Eintrag make, der nun im Lexikon (Dictionary.app) gefunden werden kann. Wird das Lexikon nicht angezeigt, muss es in den Einstellungen aktiviert werden.

Datenimport aus HanDeDict

In einem importer-Skript beginnen wir mit der Shebang-Zeile, die angibt, dass das Skript ein Ruby-Skript ist. danach schalten wir den UTF-8 Support von Ruby ein:

#!/usr/bin/env ruby
$KCODE = 'u'

Vom Terminal aus machen wir die Datei ausführbar:

chmod u+x importer

Zur Erzeugung der XML-Datei, aus der das Dictionary Development Kit den Inhalt des Lexikons liest, verwenden wir die Builder-Library von Jim Weirich. Sollte diese noch nicht installiert sein, hilft

sudo gem install builder

Dann importieren wir RubyGems und laden die Builder-Library:

require 'rubygems'
require 'builder'
Anschließend kopieren wir die UTF8-Version des HanDeDict namens handedict_nb.u8 in unser Projektverzeichnis, und lesen sie mittels Ruby-Code ein:
vocabulary = []

File.new(File.dirname(__FILE__) + '/handedict_nb.u8', 'r').each do |line|
  next if line !~ /^([^\s]+)\s+([^\s]+)\s+\[([^\]]+)\]\s+\/(.+)\/$/

  vocabulary << {:traditional => $1, 
    :simplified => $2, :pinyin => $3, :translations => $4.split('/')}
  end  
Um die XML-Datei zu erzeugen, verwenden wir die Builder-Library. Das Format schauen wir uns aus der umbenannten HanDeDict.xml ab:
File.open(File.dirname(__FILE__) + '/HanDeDict.xml', 'w') do |file|
  b = Builder::XmlMarkup.new(:target => file, :indent => 2)
  b.instruct!
  b.d :dictionary, :xmlns => "http://www.w3.org/1999/xhtml", 
      :'xmlns:d' => "http://www.apple.com/DTDs/DictionaryService-1.0.rng" do
    vocabulary.each_with_index do |entry, index|
      b.d :entry, :id => "handedict_#{index}", 
          :'d:title' => entry[:simplified] do
        b.d :index, :'d:value' => entry[:simplified]
        b.p do
          b.span entry[:simplified], :class => "entry_heading"
          b.text! " [#{entry[:pinyin]}]"
        end
        b.ol do
          entry[:translations].each do |translation|
            b.li translation
          end
        end
      end
    end
  end
end

Die Struktur ist recht einfach: Ein Lexikon dictionary enhält Einträge entry mit eindeutiger ID (Attribut id) und Titel (Attribut title). Innerhalb eines entry befinden sich ein oder mehrere Index-Einträge index (mit Attribut value) sowie der Inhalt des Eintrags. Das Dictionary Services Programming Guide gibt einen kompletten Überblick über das XML-Format.

Nun können wir die XML-Datei durch Aufruf des Skripts aufbauen und anschließend installieren:

./importer && make && make install

Leider hat diese Lösung noch ein Problem: UTF-8 Zeichen werden von der Builder-Library codiert (z.B. &#21621; statt 呵). Damit kann das Dictionary Development Kit nicht umgehen. Abhilfe schafft die neuste Version der Builder-Library aus dem Repository, die UTF-8 Zeichen unverändert lässt:

svn export svn://rubyforge.org/var/svn/builder/trunk builder
cd builder
rake builder:gem
sudo gem install pkg/builder-2.2.0.gem
cd ..

Nach einem erneuten ./importer && make && make install ist das Lexikon einsatzbereit.

Feinschliff

Vor der Veröffentlichung des Lexikons sollten wir noch einige Korrekturen durchführen: Die Einträge werden zu klein angezeigt, und wir müssen noch das Copyright angeben.

Um die Einträge größer anzuzeigen, ändern wir HanDeDict.css ab, so dass die Bereiche mit der CSS-Klasse entry_heading größer angezeigt werden. Mit dieser Klasse haben wir die Einträge im XML markiert.

@charset "UTF-8";
@namespace d url(http://www.apple.com/DTDs/DictionaryService-1.0.rng);

span.entry_heading {
  font-size: 150%;
  font-weight: bold;
}

In der Property List HanDeDict.plist setzen wir Attribute wie den Bundle Identifier, den Bundle Name, die Version und die Copyright-Information, und löschen nicht verwendete Attribute:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>Germany</string>
	<key>CFBundleIdentifier</key>
	<string>de.tammofreese.dictionary.HanDeDict</string>
	<key>CFBundleName</key>
	<string>HanDeDict</string>
	<key>CFBundleShortVersionString</key>
	<string>1.0</string>
	<key>DCSDictionaryCopyright</key>
	<string>© 2003-2008 Chinesisch-Deutsche Gesellschaft e.V. Hamburg, Tammo Freese. Some rights reserved. See http://creativecommons.org/licenses/by-sa/2.0/de/</string>
	<key>DCSDictionaryManufacturerName</key>
	<string>Chinesisch-Deutsche Gesellschaft e.V. Hamburg, Tammo Freese</string>
</dict>
</plist>

Weitere, hier nicht im Detail gezeigte Korrekturen sind die Darstellung der Tonangaben mit Tonzeichen statt Zahlen (z.B. guó statt guo2), und die Indizierung und Anzeige der traditionellen Schreibweise der chinesischen Zeichen (falls vorhanden).

Jetzt erzeugen wir das Lexikon neu und haben eine zur Veröffentlichung geeignete Version.

Download

Das HanDeDict für Apple Dictionary kann hier heruntergeladen werden: HanDeDict.dictionary.zip (33,8 MB). Zur Installation muss das Verzeichnis HanDeDict.dictionary nach ~/Library/Dictionaries oder nach /Library/Dictionaries kopiert werden. Wird es nach ~/Library/Dictionaries kopiert, ist das Lexikon nur für den aktuellen Benutzer verfügbar. Das Kopieren in das Verzeichnis /Library/Dictionaries stellt das Lexikon für alle Benutzer zur Verfügung, allerdings sind für den Kopiervorgang Administratorrechte erforderlich. Wird das Lexikon nicht angezeigt, muss man es noch in den Einstellungen von Dictionary.app aktivieren.

Verwendung des HanDeDict für Apple Dictionary

Lexika sind ein schönes Beispiel dafür, wie weit Apple die Integration von Daten innerhalb des Systems treibt:

  1. Das Lexikon kann in der Lexikon-Applikation (Dictionary.app) verwendet werden:
  2. Auch im Dashboard-Widget steht es zur Verfügung:
  3. Über die Spotlight-Suche werden die Einträge im Lexikon ebenfalls gefunden:
  4. Über das Kontextmenü in Safari und vielen anderen Anwendungen kann mittels Look Up in Dictionary in den Lexika gesucht werden:
  5. Schließlich kann über das Tastaturkürzel ctrl-command-d das Wort unter dem Mauszeiger im Lexikon gesucht werden. Zur Anzeige dient hier nicht Dictionary.app, sondern ein Popup. Das Kürzel funktioniert auch in Anwendungen, in denen dem Kontextmenü der Menüpunkt Look Up in Dictionary fehlt:
]]>
Sat, 22 Mar 2008 13:30:00 +0100 http://tammofreese.de/2008/03/22/handedict-fuer-apple-dictionary
Fortgeschrittene Techniken der testgetriebenen Entwicklung http://tammofreese.de/2007/05/30/fortgeschrittene-techniken-der-testgetriebenen-entwicklung

Im letzten Monat fand die diesjährige JAX statt. Johannes Link und ich hatten einen Power Workshop zu fortgeschrittenen Techniken der testgetriebenen Entwicklung.

Umbesetzung am frühen Morgen

Leider musste mir Johannes erkältungsbedingt am Morgen des Workshops absagen. Prinzipiell kein Problem, einer der Gründe dafür, einen Workshop zu zweit einzureichen, ist schließlich Ausfallsicherheit. Nur wurden uns wenige Tage vor dem Workshop voraussichtliche 70 Teilnehmer angekündigt, und bei dem hohen Anteil von praktischen Übungen wäre das für mich alleine doch recht hektisch geworden.

Glücklicherweise traf ich beim Frühstück Stefan Roock, einen meiner Freunde von it-agile aus Hamburg. Er meldete sich freiwillig, mir bei den Übungen auszuhelfen.

Warum testgetriebene Entwicklung?

Meine Lieblingsfrage bei Testworkshops ist immer, warum wir eigentlich testgetrieben entwickeln sollen. Tests sieht jeder ein, automatisierte Tests auch, warum aber testgetrieben?

Meine Antwort darauf: Die Relevanz von Testergebnissen hängt unter anderem von zwei Dingen ab — sie müssen möglichst aktuell sein, und möglichst viele Teile des Codes erreichen. Sind die Testergebnisse alt, machen sie nur noch eine Aussage über das System zum Zeitpunkt der Testausführung, und dieses System kann stark vom aktuellen System abweichen. Decken die Tests nur einen geringen Teil des Systems ab, haben die Ergebnisse nur wenig Aussagekraft über das Gesamtsystem.

Entwickeln wir testgetrieben, dann führen wir die Tests sehr häufig aus, damit haben wir immer aktuelle Ergebnisse. Und das testgetriebene Vorgehen führt zu einer hohen Testabdeckung: Halten wir sie komplett durch, werden 100% des Systems durch die Tests erreicht.

Eine Folgefrage ist oft, ob testgetriebene Entwicklung nicht zu teuer ist. Schließlich kostet die Testerstellung Zeit. Als Antwort darauf wird oft argumentiert, die Investition mache sich später durch höhere Qualität bezahlt. Das denke ich auch, aber empirische Untersuchungen dazu sind rar. Andererseits sollten wir lieber erst einmal in Tests investieren und sehen, wann wir zu dem Punkt kommen, an dem wir denken: Unsere Qualität ist zu hoch, wir sollten weniger Tests schreiben.

Groovy

Um den Aufwand für die Erstellung der Tests in Java zu verringern, kann man die Tests in Groovy, einer dynamisch typisierten Skriptsprache für die Java Virtual Machine, implementieren.

Natürlich können wir auch Systemteile oder das gesamte System in Groovy statt Java schreiben. Oftmals wird der Code dadurch kürzer und besser verständlich. Ein Beispiel: Nehmen wir an, wir haben in der Variablen users eine Liste von Objekten der Klasse User. Bei User-Objekten gibt der Getter getFirstName() den Vornamen zurück. Wir wollen nun alle Vornamen alphabetisch sortiert durch Kommata getrennt der Variablen firstNamesForDisplay zuweisen.

In Java kann das zum Beispiel so aussehen:

List<String> firstNames = new ArrayList<String>();
for (User user : users) {
    firstNames.add(user.getFirstName());
}  
Collections.sort(firstNames);
StringBuffer result = new StringBuffer();
for (Iterator<String> it = firstNames.iterator(); it.hasNext();) {
  result.append(it.next());
  if (it.hasNext()) result.append(", ");
}
String firstNamesForDisplay = result.toString();

Sicherlich kann man den Code noch ein wenig reduzieren, aber der Groovy-Code wird immer schöner bleiben:

String firstNamesForDisplay = users.firstName.sort().join(', ')

Im Workshop kam die Frage auf, Groovy der Nachfolger von Java sein wird. Natürlich ist jede Antwort auf so eine Frage ein Blick in die Kristallkugel. Meiner Meinung nach hat Groovy das Potenzial, einen großen Teil der Java-Entwicklung zu ersetzen, da man mit viel weniger Code viel mehr erreichen kann.

Leider hat eine dynamisch getypte Sprache Nachteile, wenn es um die Werkzeugunterstützung geht. Eine gute Code Completion zu implementieren, ist schwierig, da die Typen von Variablen zur Übersetzungszeit nicht bekannt sind. Auch automatisierte Refactorings sind durch die dynamischen Eigenschaften der Sprache schwieriger zu implementieren, und bleiben wohl am Ende immer unzuverlässiger als für Java.

Infos zum Workshop

Johannes hat unsere Folien zum Workshop auf SlideShare veröffentlicht. Stefan ließ es sich nicht nehmen, über die Inhalte des Workshops zu bloggen (Teil 1, Teil 2, Teil 3). Auch einer der Teilnehmer fasste den Workshop in einem Blog-Eintrag zusammen.

]]>
Wed, 30 May 2007 21:07:00 +0100 http://tammofreese.de/2007/05/30/fortgeschrittene-techniken-der-testgetriebenen-entwicklung
Tracking zur einfachen Aufwandsschätzung http://tammofreese.de/2007/05/28/tracking-zur-einfachen-aufwandsschaetzung

Für eine zuverlässige Projektplanung müssen wir zuverlässige Schätzungen abgeben. Im Extreme Programming wollen wir uns dabei nicht nur auf unser Bauchgefühl verlassen, sondern Aufwände für zukünftige Aufgaben aus den Aufwänden für bereits erfüllte Aufgaben ableiten.

Warum überhaupt Tracking?

Im Tracking erfassen wir die geschätzten und tatsächlichen Aufwände für die Erfüllung von Aufgaben, und messen damit die Entwicklungsgeschwindigkeit. Sie ist sowohl für den Kunden als auch für die Entwicklung eine wichtige Information:

  • Der Kunde erfährt, wie viele Aufgaben in den nächsten Entwicklungszyklen voraussichtlich erfüllt werden können.
  • Die Entwickler erhalten Informationen, ob und wie sich die Entwicklungsgeschwindigkeit ändert, woraus sie auf Probleme im Entwicklungsprozess schließen können.

Das einfachste Tracking, das funktionieren könnte

Das einfachste Maß für die Entwicklungsgeschwindigkeit ist die Anzahl von Aufgaben, die pro Entwicklungszyklus realisiert werden. Für den Kunden ist dieses Maß ebenfalls geeignet, da es den für ihn sichtbaren Projektfortschritt abbildet.

Für den folgenden Entwicklungszyklus nehmen wir dann die gleiche Geschwindigkeit an, die wir im letzten Durchlauf erreicht haben. Das ist die ehrlichste Abschätzung, die wir bei diesem Maß machen können.

Im Extreme Programming hat sich dafür der Begriff Yesterday's Weather eingebürgert: Die Vorhersage, dass das Wetter heute genauso sein wird wie gestern, erreicht eine recht hohe Genauigkeit.

Verschiedene Aufgabengrößen

Variieren die Aufwände pro Aufgabe stark und lassen sich die Aufgaben partout nicht in annähernd gleichgroße, für den Kunden bedeutsame Teile zerschneiden, führen wir ein Maß für die Größe von Aufgaben ein. Wir geben zuerst einer kleinen Aufgabe 1 Punkt und schätzen dann größere Aufgaben im Vergleich damit ab: Schätzen wir eine andere Aufgabe doppelt so groß, bekommt sie 2 Punkte, schätzen wir eine andere wiederum doppelt so groß, geben wir ihr 4 Punkte.

Die Geschwindigkeit messen wir dann in Punkten pro Entwicklungszyklus. Für den folgenden Durchlauf nehmen wir die Geschwindigkeit des letzten Durchlaufs an.

Schätzungen und Realdaten

Der Anteil am tatsächlichen Aufwand einer Aufgabe am Gesamtaufwand eines Entwicklungsdurchlaufs kann stark von dem Punktanteil an der Gesamtpunktzahl abweichen. Zum Beispiel kann eine Aufgabe mit nur 10% der Punkte abgeschätzt worden sein, aber tatsächlich 30% der Zeit gekostet haben.

Treten solche Abweichungen häufig auf, wird das Tracking unzuverlässig. Um das Problem zu lösen, erfassen wir pro Aufgabe, wieviel Zeit tatsächlich darauf entfallen sind, und verteilen für zukünftige Vergleiche die geleisteten Punkte nach der tatsächlich verwendeten Zeit auf die Aufgaben.

Zur Vermeidung von Pseudogenauigkeiten wollen wir weiterhin mit vollen Punktzahlen arbeiten. Daher verwenden wir ein einfaches Verfahren, um die geleisteten Punkte auf die Aufgaben umzurechnen:

  1. Jede Aufgabe bekommt den ganzzahligen Anteil.
  2. Verbleibende Punkte werden den Aufgaben mit den höchsten Teilungsresten zugeschlagen.

Als Beispiel nehmen wir an, dass im letzten Entwicklungszyklus die Aufgaben A, B, C, D und E mit jeweils 2 Punkten geplant waren. Tatsächlich war noch etwas Zeit übrig, so dass die 1-Punkt-Aufgabe F ebenfalls erfüllt werden konnte. Insgesamt wurden also 11 Punkte geliefert. Mit den realen Aufwänden verteilen wir diese nun auf die Aufgaben:

Aufgabe A: 11 Punkte * 23% = 2,53 Punkte
Aufgabe B: 11 Punkte * 16% = 1,76 Punkte
Aufgabe C: 11 Punkte * 25% = 2,75 Punkte
Aufgabe D: 11 Punkte * 16% = 1,76 Punkte
Aufgabe E: 11 Punkte *  5% = 0,55 Punkte
Aufgabe F: 11 Punkte * 15% = 1,65 Punkte

Jede Aufgabe bekommt die ganzzahligen Punkte, das sind insgesamt 7 Punkte. Die restlichen 4 Punkte bekommen die Aufgaben mit den höchsten Teilungsresten, also B, C, D und F:

Aufgabe A: 2 + 0 Punkte = 2 Punkte
Aufgabe B: 1 + 1 Punkt  = 2 Punkte
Aufgabe C: 2 + 1 Punkt  = 3 Punkte
Aufgabe D: 1 + 1 Punkt  = 2 Punkte
Aufgabe E: 0 + 0 Punkte = 0 Punkte
Aufgabe F: 1 + 1 Punkt  = 2 Punkte

Stellen wir die geschätzten Punkte den nach realen Aufwänden verteilten gegenüber, sehen wir, wie gut unsere Schätzungen im Einzelnen waren. Hier waren die Schätzungen für A, B und D recht gut, C und F haben wir unterschätzt und E völlig überbewertet.

Das hier verwendete Vorgehen, um Prozentwerte auf eine feste Anzahl von Punkten zu verteilen, nennt sich Hare-Niemeyer-Verfahren und wird bei Parlamentswahlen dazu verwendet, Sitze auf Parteien zu verteilen.

Einfache Ausnahmebehandlung

Zahlreiche geplante und ungeplante Ereignisse können die pro Durchlauf verfügbare Zeit verändern (Krankheit, Urlaub, neue Teammitglieder, …). Natürlich könnten wir diese Daten verwenden, um zu versuchen, die zukünftige Geschwindigkeit genauer einzuschätzen.

Viel einfacher ist aber, einfach einen Entwicklungsdurchlauf zu warten und damit festzustellen, ob sich die Geschwindigkeit tatsächlich ändert.

Analog gehen wir mit Aufwänden um, die wir nicht direkt den bearbeiteten Aufgaben zuweisen können. Sagen wir mal, wir haben 10 Punkte geschätzt, eine der letzten Lieferungen war aber fehlerhaft und 20% der Zeit sind in ungeplante Fehlerkorrekturen geflossen. Außerdem haben wir 10% der Zeit in Meetings verbracht. Tatsächlich haben wir 7 Punkte geschafft. Natürlich können wir nun zusätzliche 'Aufgaben' für die Fehlerkorrekturen (2 Punkte) und die Meetings (1 Punkt) erfassen und damit unsere tatsächliche Geschwindigkeit auf die geschätzten 10 Punkte bringen.

Tatsächlich ist so eine Vorgehensweise fatal: Wer sagt uns, dass wir im nächsten Durchlauf nicht wieder 20% der Zeit für ungeplante Fehlerkorrekturen brauchen? Und wer sagt uns, dass wir mit weniger Meetings nicht noch weniger Punkte geschafft hätten, da zu wenig Abstimmung vorlag?

Wir erfassen unsere Geschwindigkeit also mit ehrlichen 7 Punkten. Die Zeiten für nicht aufgabenrelevante Aufwände schlagen wir einfach den Aufgaben zu, von denen uns die Aufwände abgehalten haben.

Anpassung der Planung

Natürlich eignet sich die Ausnahmebehandlung nur für Ausnahmefälle. Haben wir ständig wiederkehrende Aufwände für ungeplante Aufwände (zum Beipiel Serveradministration und Support), bietet es sich an, diese Aufgaben entweder aus der Entwicklung herauszulösen, oder sie explizit einzuplanen.

Geschwindigkeitsänderungen

Bei der Geschwindigkeit sollte keine Konstante erwartet werden, kleine Schwankungen sind völlig normal. Bei starken Geschwindigkeitsänderungen sollten wir versuchen, den Ursachen auf den Grund zu gehen. Zum Beispiel mag eine plötzliche Geschwindigkeitssteigerung aus Kundensicht schön aussehen, sie kann aber ebensogut bedeuten, dass zu viel Druck auf das Team ausgeübt wurde, und dass darunter die strukturelle Qualität des Systems und die Testabdeckung gelitten haben.

Lieber wenig als überhaupt nicht

Sicher, das hier vorgestellte Tracking ist bewusst einfach und ungenau. Andererseits können komplexere Lösungen leicht zu bürokratischen Monstern ausarten, die vom Team nicht akzeptiert werden und daran scheitern.

Meine Empfehlung ist daher: Starten Sie lieber mit dem einfachsten Tracking, das funktionieren könnte. Erweitern Sie es dann, wenn es tatsächlich nötig ist. Und prüfen Sie, ob sie es später nicht wieder vereinfachen können.

]]>
Mon, 28 May 2007 13:55:00 +0100 http://tammofreese.de/2007/05/28/tracking-zur-einfachen-aufwandsschaetzung
Ruby on Rails Fixtures – Fluch oder Segen? http://tammofreese.de/2006/10/29/ruby-on-rails-fixtures-fluch-oder-segen

Ruby on Rails baut auf einer Model-View-Controller (MVC)-Architektur auf. Die Modellschicht ActiveRecord verwendet das gleichnamige Pattern Active Record, das in Martin Fowlers Buch Patterns of Enterprise Application Architecture dokumentiert ist. Hierbei wird eine Datenbankrelation durch eine Klasse abgebildet, eine Zeile der Relation entspricht einer Instanz der Klasse. Eine Ausnahme von dieser Regel sind die Tabellen, die m:n-Abbildungen abbilden.

Test-Fixtures in Rails

Rails bietet Tests auf drei Ebenen an:

  • Unit Tests testen die Modellobjekte, also die ActiveRecord-Schicht.
  • Funktionale Tests testen einzelne Controller. Dabei verwenden diese die Modellobjekte. In den Tests können auch die Views mit abgedeckt werden.
  • Integrationstests testen das System Controller-übergreifend.

Alle drei Testarten verwenden also die Modellobjekte. Die meisten sinnvollen Tests benötigen Daten, auf denen sie operieren. Die Standardlösung dafür in Rails sind Fixtures. In diesen Dateien werden Objekte typischerweise im YAML-Format abgelegt:

john:
  id: 1
  name: John Doe
  login: johndoe
  password: nosecret

In den Testklassen werden dann die benötigten Fixtures über die fixtures-Methode eingebunden. Sie stellt sicher, dass die Daten aus den übergebenen Fixtures jedem Test der Testfallklasse in der Datenbank zur Verfügung stehen. Die Datensätze können dann im Test über eine Methode mit dem Namen der Fixture zugegriffen werden:

  fixtures :users	
	
  def setup
    @john = users(:john)	
  end  

Probleme durch Fixtures

Die Fixtures können zu zahlreichen Problemen führen:

  • Tests können ohne die Kenntnis der Fixture-Daten unverständlich sein.
  • Die Fixtures sind global, enthalten also Daten für alle Tests. Das kann dazu führen, dass Abhängigkeiten zwischen Tests entstehen.
  • m:n-Beziehungen in Fixtures zu definieren erfordert eine Menge an Datensätzen.
  • Die Daten aus den Fixtures werden an den Rails-Mechanismen vorbei in die Datenbank geschrieben. Damit können Testdaten ungültige Datensätze sein.

Best Practices

Um die Probleme mit Fixtures in den Griff zu bekommen, bieten sich einige Techniken an:

Minimale Fixture

In den Fixtures sollten nur diejenigen Datensätze definiert werden, die für einen Großteil der Tests relevant sind. Wird zum Beispiel mit Authentifizierung gearbeitet, könnte ein Benutzerkonto zu diesen Daten gehören.

Damit wird die Menge an Fixture-Daten drastisch reduziert, und deutlich überschaubarer.

Echte Unit Tests schreiben

Für viele Tests ist es nicht notwendig, Datenbankobjekte in die Datenbank zu speichern. Nehmen wir als Beispiel diesen (durchwachsenen) Unit Test:

def test_card_validates_presence_of_front_and_back
  card = Card.new
  assert !card.save

  card.front = '1 + 3 = ?'
  assert !card.save

  card.front = ''
  card.back = '4'
  assert !card.save
  
  card.front = '1 + 3 = ?'
  assert card.save
end

Die Validations können hier ebensogut über den Aufruf der Methode valid? getestet werden, was auch erlaubt, den Test in zwei Fälle aufzuteilen:

def test_card_validates_presence_of_front
  card = Card.new
  card.valid?
  assert card.errors.include?(:front)
  card.front = 'front' 
  card.valid?
  assert !card.errors.include?(:front)
end

def test_card_validates_presence_of_back
  card = Card.new
  card.valid?
  assert card.errors.include?(:back)
  card.back = 'back' 
  card.valid?
  assert !card.errors.include?(:back)
end

Test Helper

Ist das Anlegen von testspezifischen Daten nicht vermeidbar, werden sie mit Hilfsmethoden im Testsetup oder direkt im Test angelegt. Ein gutes Schema für die Hilfsmethoden ist:

  • Hilfsmethoden heißen create_modellname(options = {}). Sie erzeugen ein Objekt des Typs Modellname in der Datenbank und geben es zurück.
  • Bei Aufruf ohne Optionen wird durch die Methode ein neues Objekt angelegt. Attribute werden so mit Standardwerten belegt, dass alle Nebenbedingungen (validations) eingehalten werden. Beziehungsobjekte werden entweder aus einer Fixture referenziert, oder aber durch den entsprechenden Helper neu angelegt.
  • Attribute und Beziehungsobjekte können alternativ über das options-Hash mitgegeben werden.

Mit den Hilfsmethoden können wir nun Tests schreiben, bei denen die Fixture in der setup-Methode der Testklasse oder in den einzelnen Testmethoden aufgebaut wird.

Fluch oder Segen?

Rails Fixtures sind ein Segen, da sie uns erlauben, häufig in Tests benötigte Datensätze einmalig anzulegen. Sie werden dann zum Fluch, wenn sich testspezifische Daten in die Fixtures verirren.

]]>
Sun, 29 Oct 2006 09:00:00 +0100 http://tammofreese.de/2006/10/29/ruby-on-rails-fixtures-fluch-oder-segen
QYPE ist live http://tammofreese.de/2006/05/01/qype-ist-live

Seit letzten Dienstag (25.04.2006) ist QYPE [kwaɪp] online, die Plattform, an der ich seit drei Monaten mitarbeite. Auf QYPE kann jeder Benutzer die Adressen seiner Stadt bewerten und mit Stichworten (Tags) versehen, und über die Tags, Städte und Menschen auf QYPE neue Adressen entdecken.

Über QYPE berichteten unter anderem Spiegel Online und n-tv.de. Als Projekt ist QYPE für mich interessant, weil ich dort mit guten Leuten in einem echten Team nach Extreme Programming-Prinzipien arbeiten kann. Und weil wir auf Ruby on Rails und AJAX setzen, um eine hohe Entwicklungsgeschwindigkeit zu erreichen. Und weil wir auf Mac OS X entwickeln.

]]>
Mon, 01 May 2006 20:00:00 +0100 http://tammofreese.de/2006/05/01/qype-ist-live
CSS Min-Height Simulation für Internetseiten http://tammofreese.de/2005/12/31/css-min-height-simulation-fuer-internetseiten

Um für Elemente einer HTML-Seite eine minimale Höhe anzugeben, gibt es in CSS 2.0 die min-height-Eigenschaft. Leider wird diese nicht vom Microsoft Internet Explorer 6.0 unterstützt.

CSS Min-Height Simulation für feste Pixelwerte

Auf http://www.greywyvern.com/code/min-height-hack.html wird der CSS min-height Hack beschrieben, der erlaubt, für ein div-Element auch ohne Verwendung der min-height-Eigenschaft eine minimale Höhe in Pixeln anzugeben.

Als Beispiel dient ein div-Element, für das eine minimale Höhe von 50 Pixeln angegeben werden soll:

<div>
  Inhalt
</div>

Die minimale Höhe wird über zwei zusätzliche div-Element und CSS-Code erzwungen.

<div>
  <div class="prop"></div>
  Inhalt
  <div class="clear"></div>
</div>

Das Element mit der Klasse prop bekommt die gewünschte Höhe zugewiesen und wird nach rechts verschoben.

.prop {
  height: 50px;
  float: right;
  width: 1px;
}

Das Element mit der Klasse clear wird nach dem Originalartikel für ältere Mozilla- und Firefox-Versionen benötigt, damit der Inhalt durch das prop-Element bis nach unten reicht. Die overflow-Eigenschaft soll gesetzt werden, da der Microsoft Internet Explorer 6.0 im Standard-kompatiblen Modus div-Elemente mit einer Höhe von weniger als 1em nicht automatisch erzeugen soll.

.clear {
  clear:both;
  height:1px;
  overflow:hidden;
}

Hier sind drei Beispiele für den CSS min-height Hack, eine leere Box, eine Box, bei die niedriger als 50 Pixel sein sollte, und eine, die höher sein sollte. Zur Verdeutlichung ist die prop-Klasse rechts und die clear-Klasse unten in dunkelgrau dargestellt:

Weniger als 50px Höhe
Das ist hoffentlich genug Text für mehr als 50px Höhe

Anpassung der Simulation für Internetseiten

Um für eine komplette HTML-Seite zu erreichen, dass sie mindestens die Höhe des Browserfensters einnimmt (dass also die Fußzeile auch für Seiten mit kurzem Inhalt immer unten im Browserfenster erscheint), muss der CSS min-height Hack modifiziert werden.

Ausgangspunkt

Statt eines div-Elements ist der Inhalt einer HTML-Seite in ein body-Element eingeschlossen:

<body>
  Inhalt
</body>

Der Text einer HTML-Seite darf nicht direkt im body-Element stehen, daher wird er in ein div-Element eingeschlossen. Zusätzlich nehmen wir ein div-Element für die Fußzeile auf:

<body>
  <div>
    Inhalt
  </div>
  <div>Fußzeile</div>
</body>

Um einen Ausgangspunkt zu haben, der dem des CSS min-height Hack ähnelt, führen wir ein div-Element mit der Klasse prop ein, und versehen das div-Element der Fußzeile mit der Klasse clear. Da die Elemente auf der Seite eindeutig sein sollten, ist es sinnvoll, statt dem class-Attribut das id-Attribut zu verwenden. Um uns am Original zu halten, lassen wir es in diesem Beispiel aber sein.

<body>
  <div class="prop"></div>
  <div>
    Inhalt
  </div>
  <div class="clear">Fußzeile</div>
</body>

Im CSS passen wir die Höhe für die prop-Klasse die Höhe auf 100% an. Für die clear-Klasse setzen wir die Höhe der Fußzeile (hier auf 100px), und löschen die overflow-Eigenschaft, da diese mit der neuen Höhe nicht mehr benötigt wird:

.prop {
  height: 100%;
  float: right;
  width: 1px;
}

.clear {
  clear:both;
  height:100px;
}

Problem 1: Zu niedrig

Am Ausgangspunkt hängt die Fußzeile direkt unter dem Inhalt und nicht am unteren Bereich des Fensters. Das liegt daran, dass nach der CSS2-Spezifikation das prop-Element nun 100% der Höhe des umschließenden Blocks, also des body-Elements bekommt. Das hat standardmäßig die Höhe auto, nimmt also nur soviel Platz ein, wie nötig. Wir müssen also für das body-Element eine Höhe von 100% zuweisen. Für die Kompatibilität zu Mozilla und Safari muss auch das umschließende html-Element die Höhe 100% zugewiesen bekommen:

html, body {
   height: 100%;
}   

Problem 2: Zu hoch

Vom Regen in die Traufe: Jetzt ist die Fußzeile zwar unten, aber zu weit: Die Seite ist immer höher als der zur Verfügung stehende Platz.

Ein Teil der zu großen Höhe liegt an den Standardwerten für Innen- und Außenabstand des body-Elements. Also setzen wir Innen- und Außenabstand auf 0:

body {
  margin: 0;
  padding: 0;
}

Das Problem ist kleiner geworden, die Seite ist aber immer noch zu hoch. Das liegt daran, dass unser prop-Element 100% der Seitenhöhe bekommt, und zusätzlich noch die Fußzeile mit 100 Pixel zu Buche schlägt. Die Seite ist also um 100 Pixel zu hoch.

Über die Höhe des prop-Elements können wir das Problem nicht lösen, da wir eine relative Größe in Prozent von einer absoluten Größe in Pixeln subtrahieren müssten. Wollen wir die absolute Höhe der Fußzeile beibehalten, müssen wir die Subtraktion in einer anderen Weise vornehmen. Das erreichen wir mit dem Außenabstand des prop-Elements: Indem wir ihn unten auf -100 Pixel setzen, werden unten anliegende Elemente beim Layout, also in unserem Fall die Fußzeile, 100 Pixel höher angesetzt:

.prop {
  height: 100%;
  float: right;
  width: 1px;
  margin: 0 0 -100px 0;
}

Die Min-height Simulation ist fertig

Damit ist das Ziel erreicht: Solange das Browserfenster groß genug ist, bleibt die Fußzeile am unteren Rand. Wird das Browserfenster zu klein für die Seite, erscheinen Scrollbalken. Diese min-height Simulation wurde mit Microsoft Internet Explorer 6 auf Windows XP SP 2 sowie mit Safari 2.0.2 und Firefox 1.0.6 auf Mac OS X 10.4.3 erfolgreich getestet.

Auf meiner Internetpräsenz http://tammofreese.de ist eine Variante der min-height Simulation eingesetzt, die auch eine Kopfzeile berücksichtigt. Beim div-Element, das die minimale Höhe garantiert, wird dafür auch oben ein negativer Außenabstand verwendet.

]]>
Sat, 31 Dec 2005 14:00:00 +0100 http://tammofreese.de/2005/12/31/css-min-height-simulation-fuer-internetseiten
EasyMock 2.0 http://tammofreese.de/2005/12/24/easymock-2-0

Mit EasyMock können Mock-Objekte für automatisierte Tests dynamisch generiert werden. Die neue Version EasyMock 2.0 setzt Java 5.0 voraus und kann unter http://www.easymock.org/Downloads.html heruntergeladen werden.

Unit Tests und Mock-Objekte

In Unit Tests sollen Elemente der Software isoliert getestet werden. Ein Element funktioniert oft nur in Zusammenarbeit mit anderen Elementen. Um es in Isolation zu testen, müssen diese Mitarbeiter simuliert werden.

Ein Mock-Objekt ersetzt einen Mitarbeiter im Test. Es wird mit erwarteten Methodenaufrufen konfiguriert. Im Test überprüft es, ob die erwarteten Methodenaufrufe auftreten. Bei Abweichungen vom erwarteten Verhalten schlägt es so früh wie möglich fehl.

Mock-Objekte mit EasyMock 2.0

Um EasyMock 2.0 in Tests zu verwenden, müssen die Methoden einer Klasse importiert werden:

import static org.easymock.EasyMock.*;	

Ein Mock-Objekt für eine Schnittstelle kann dann mit einer Programmzeile dynamisch erzeugt werden:

IWebSearch mock = createMock(IWebSearch.class);

Nach der Erzeugung des Mock-Objekts befindet es sich in der Aufnahmephase. Methodenaufrufe auf dem Mock-Objekt werden als Erwartungen interpretiert:

mock.registerURL("http://www.fscklog.com");

Rückgabewerte können mit einer einfachen Syntax definiert werden:

expect(mock.search("ruby rails")).andReturn("http://www.rubyonrails.org");

Ist das Mock-Objekt mit dem erwarteten Verhalten gefüttert, wird es in die Wiedergabephase umgeschaltet:

replay(mock);

Nun kann das Mock-Objekt im Test verwendet werden. Wird eine nicht erwartete Methode oder eine Methode mit anderen Argumenten als den erwarteten aufgerufen, wird eine Exception geworfen. Am Ende des Tests sollte noch geprüft werden, ob die erwarteten Methodenaufrufe tatsächlich stattgefunden haben. Das erledigt der Aufruf

verify(mock);

Komplexe Mock-Objekte

Standardmäßig werden die Argumente der erwarteten Methodenaufrufe mit equals() verglichen. Falls es gewünscht ist, können diese Bedingungen abgeschwächt werden. Auch die Anzahl der erlaubten Aufrufe kann geändert werden. Das folgende Programmstück definiert die Erwartung, das die Methode search() mindestens einmal mit einem Argument aufgerufen wird, das die Begriffe ruby und rails enthält. Für alle Aufrufe wird http://www.rubyonrails.org zurückgegeben:

expect(mock.search(and(contains("ruby"), contains("rails"))))
    .andReturn("http://www.rubyonrails.org").atLeastOnce();

Weitere Informationen

Die Dokumentation von EasyMock demonstriert alle Möglichkeiten von EasyMock. Sie ist im Download enthalten und online verfügbar unter http://easymock.org/EasyMock2_0_Documentation.html.

Im Buch Testgetriebene Entwicklung mit JUnit & FIT beschreibt Frank Westphal ausführlich, wie Mock-Objekte helfen können, Teile eines Systems im Unit Test isoliert zu testen.

]]>
Sat, 24 Dec 2005 15:00:00 +0100 http://tammofreese.de/2005/12/24/easymock-2-0