├── cover.png ├── leanpub ├── highlighting-ch05.txt ├── highlighting-ch06.txt ├── highlighting-ch04.txt ├── highlighting-ch09.txt ├── STEPS ├── get_sources.pl └── convert.pl ├── cc-by-sa.png ├── cc-by-sa-big.png ├── .gitignore ├── README.md ├── cover.html ├── fix-epub-toc.py ├── single-html-listing-title-hack.pl ├── add-epub-cover.sh ├── documentacion_activa-biblio.asc ├── set_pdf_metadata.bash ├── calidad-biblio.asc ├── chunked-html-listing-title-hack.pl ├── tdd-biblio.asc ├── integracion_continua-biblio.asc ├── consejospruebas-biblio.asc ├── funcionales-biblio.asc ├── fix-highlighting.py ├── insert_snippets.bash ├── tex-listing-title-hack.pl ├── extras ├── eclipse.asciidoc └── herramientas.asciidoc ├── Makefile ├── libro.css ├── book.asc.in ├── theme-libro └── libro.css ├── documentacion_activa.asc.in ├── calidad.asc ├── integracion_continua.asc ├── documentacion_activa.asc ├── funcionales.asc ├── tdd.asc.in └── consejospruebas.asc /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanchado/camino-mejor-programador/HEAD/cover.png -------------------------------------------------------------------------------- /leanpub/highlighting-ch05.txt: -------------------------------------------------------------------------------- 1 | java 2 | html 3 | java 4 | java 5 | java 6 | java 7 | 8 | -------------------------------------------------------------------------------- /leanpub/highlighting-ch06.txt: -------------------------------------------------------------------------------- 1 | ruby 2 | ruby 3 | ruby 4 | python 5 | python 6 | js 7 | -------------------------------------------------------------------------------- /cc-by-sa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanchado/camino-mejor-programador/HEAD/cc-by-sa.png -------------------------------------------------------------------------------- /cc-by-sa-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanchado/camino-mejor-programador/HEAD/cc-by-sa-big.png -------------------------------------------------------------------------------- /leanpub/highlighting-ch04.txt: -------------------------------------------------------------------------------- 1 | cl 2 | cl 3 | cl 4 | python 5 | python 6 | ruby 7 | javascript 8 | ruby 9 | javascript 10 | haskell 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.swp 3 | cover.pdf 4 | book.asc 5 | book.tex 6 | camino_*.pdf 7 | camino_*.html 8 | camino_*.epub 9 | camino_*.chunked 10 | camino_*.xml 11 | tex-tmp 12 | tmp 13 | epub-tmp 14 | .idea/ 15 | -------------------------------------------------------------------------------- /leanpub/highlighting-ch09.txt: -------------------------------------------------------------------------------- 1 | scala 2 | scala 3 | scala 4 | scala 5 | scala 6 | scala 7 | scala 8 | scala 9 | scala 10 | scala 11 | scala 12 | scala 13 | scala 14 | scala 15 | scala 16 | scala 17 | scala 18 | scala 19 | scala 20 | scala 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | El camino a un mejor programador 2 | ================================ 3 | 4 | Código fuente (en AsciiDoc) del libro «El camino a un mejor 5 | programador», una colección de artículos técnicos sobre ingeniería de 6 | software de Joaquín Caraballo Moreno, Yeray Darias Camacho y Esteban 7 | Manchado Velázquez. 8 | 9 | Más información en http://emanchado.github.com/camino-mejor-programador 10 | -------------------------------------------------------------------------------- /cover.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cover 5 | 6 | 7 | 8 |
9 | Title of this Thing, Second Edition 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /fix-epub-toc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | from lxml import etree 5 | 6 | toc_file = sys.argv[1] 7 | doc = etree.parse(toc_file) 8 | 9 | rootNavPoint = doc.getroot()[2][0] 10 | for navpoint in rootNavPoint: 11 | i = 0 12 | while i < len(navpoint): 13 | if navpoint[i].tag.endswith("navPoint"): 14 | del(navpoint[i]) 15 | else: 16 | i = i + 1 17 | print etree.tostring(doc) 18 | -------------------------------------------------------------------------------- /single-html-listing-title-hack.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | my $file = shift @ARGV; 7 | 8 | my $title = "YOU HAVE A BUG, SUCKER"; 9 | 10 | open F, $file; 11 | while (my $line = ) { 12 | if ($line =~ /^
/) { 13 | $title = $line; 14 | } elsif ($title && $line =~ m|
|) { 15 | $line =~ s|\s*$||; 16 | print $line; 17 | print $title; 18 | print "\n"; 19 | $title = undef; 20 | } else { 21 | print $line; 22 | } 23 | } 24 | close F; 25 | -------------------------------------------------------------------------------- /add-epub-cover.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONTENT_OPF_PATH=$1 4 | if [ ! -r $CONTENT_OPF_PATH ]; then 5 | echo "ERROR: Couldn't read $CONTENT_OPF_PATH" 1>&2 6 | exit 1 7 | fi 8 | xmllint -format $CONTENT_OPF_PATH | sed -e 's//&\n \n /' -e 's//' -e 's/<\/spine>/&\n \n \n <\/guide>/' 9 | -------------------------------------------------------------------------------- /documentacion_activa-biblio.asc: -------------------------------------------------------------------------------- 1 | [bibliography] 2 | .Documentación activa 3 | * [[[goos]]] Steve Freeman y Nat Price. «Growing Object-Oriented Software 4 | Guided by Tests». Addison-Wesley Professional. ISBN 978-0321503626. 5 | http://www.growing-object-oriented-software.com/ 6 | * [[[concordion]]] David Peterson. «Concordion». http://concordion.org/ 7 | * [[[xcordion]]] Robert Pelkey. «XCordion» (clon de Concordion). http://code.google.com/p/xcordion/ 8 | * [[[bridging]]] Gojko Adzic. «Bridging the Communication Gap». Neuri Limited. 978-0955683619. http://www.acceptancetesting.info/the-book/ 9 | 10 | -------------------------------------------------------------------------------- /set_pdf_metadata.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | title=$1 4 | author=$2 5 | url=$3 6 | 7 | [[ -z "$3" ]] && echo 'usage <author> <url> <input.pdf >output.pdf' && exit -1 8 | 9 | rm -Rf tmp-set_pdf_metadata && mkdir tmp-set_pdf_metadata && cd tmp-set_pdf_metadata 10 | 11 | cat >input.pdf 12 | 13 | pdftk input.pdf dump_data output report.txt 14 | 15 | echo -e "InfoKey: Title\nInfoValue: ${title}" >>report.txt 16 | echo -e "InfoKey: Author\nInfoValue: ${author}" >>report.txt 17 | echo -e "InfoKey: Url\nInfoValue: ${url}" >>report.txt 18 | 19 | pdftk input.pdf update_info report.txt output output.pdf 20 | 21 | cat <output.pdf 22 | -------------------------------------------------------------------------------- /calidad-biblio.asc: -------------------------------------------------------------------------------- 1 | [bibliography] 2 | .Calidad en software 3 | - [[[technopoly]]] Neil Postman. «Technopoly». Random House USA Inc. ISBN 4 | 9780679745402. 5 | - [[[theletter]]] Uncle Bob. «The Letter». 6 | http://blog.8thlight.com/uncle-bob/2012/01/12/The-Letter.html 7 | - [[[obliquestrategies]]] Brian Eno y Peter Schmidt. «Oblique Strategies». 8 | http://en.wikipedia.org/wiki/Oblique_Strategies 9 | - [[[broken]]] Seth Godin. «This is broken» (TED Talk). 10 | http://www.ted.com/talks/seth_godin_this_is_broken_1.html 11 | - [[[livingcomplexity]]] Donald A. Norman. «Living with Complexity». The MIT 12 | Press. ISBN 9780262014861. 13 | -------------------------------------------------------------------------------- /chunked-html-listing-title-hack.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | my $file = shift @ARGV; 7 | 8 | my ($chop_p, $title) = (1, "YOU HAVE A BUG, SUCKER"); 9 | 10 | open F, $file; 11 | while (my $line = <F>) { 12 | if ($line =~ /<p title=/) { 13 | if ($chop_p) { 14 | $title = <F>; 15 | $title =~ s|\...</strong>|</strong>|; 16 | $title =~ s|<(/?)strong>|<$1em>|g; 17 | <F>; # Discard the next line, should be the closing </p> 18 | } else { 19 | print $line; 20 | print $title; 21 | } 22 | $chop_p = !$chop_p; 23 | } else { 24 | print $line; 25 | } 26 | } 27 | close F; 28 | -------------------------------------------------------------------------------- /leanpub/STEPS: -------------------------------------------------------------------------------- 1 | Steps to convert the Leanpub Markdown source files (created by the 2 | HTML importer, feeding it the chunked HTML version produced by a2x, 3 | and sitting under manuscript-auto-converted/) into a final, refined 4 | version that can be used as Leanpub sources to get a final book: 5 | 6 | * Fix Book.txt (order and which files, and remove index.txt) 7 | * Make sure all source code blocks begin with " __" (first two in 8 | ch04.txt need fixing) 9 | * Remove the emphasis in titles ("^__Mal_" in ch04.txt and ch06.txt) 10 | * Run "./convert.pl manuscript-auto-converted manuscript convert_done" 11 | 12 | Now the sources under manuscript/ are ready to compile. 13 | -------------------------------------------------------------------------------- /tdd-biblio.asc: -------------------------------------------------------------------------------- 1 | [bibliography] 2 | .Desarrollo dirigido por pruebas 3 | - [[[tddbyexample]]] Kent Beck. «Test Driven Development: By example». Addison Wesley. ISBN 978-0321146533. 4 | - [[[tddequality]]] Kent Beck. «Test Driven Development: Equality for All». InformIT. http://www.informit.com/articles/article.aspx?p=30641 5 | - [[[goos-bis]]] Steve Freeman y Nat Pryce. «Growing Object-Oriented Software Guided by Tests». Addison-Wesley Professional. ISBN 978-0321503626. 6 | - [[[mockito]]] Szczepan Faber and friends. «Mockito: simpler & better mocking» (sitio del proyecto). http://code.google.com/p/mockito/ 7 | - [[[bleyco-bis]]] Carlos Blé Jurado. «Diseño ágil con TDD». ISBN 978-1445264714. http://dirigidoportests.com/el-libro 8 | -------------------------------------------------------------------------------- /leanpub/get_sources.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | my $help_string = "Need source and snippet number\n"; 7 | my $source = shift || die $help_string; 8 | my $number = shift || die $help_string; 9 | 10 | my $state = "seek"; 11 | open F, $source or die "Can't open $source"; 12 | while (my $line = <F>) { 13 | if ($line =~ /<pre class="programlisting">/) { 14 | if (--$number == 0) { 15 | $state = "print"; 16 | } 17 | } 18 | 19 | if ($state eq "print") { 20 | last if $line =~ /^<\/tt><\/pre>/; 21 | 22 | $line =~ s/<[^>]+>//go; 23 | $line =~ s/</</go; 24 | $line =~ s/>/>/go; 25 | $line =~ s/"/\"/go; 26 | $line =~ s/&/&/go; 27 | print $line; 28 | } 29 | } 30 | close F; 31 | -------------------------------------------------------------------------------- /integracion_continua-biblio.asc: -------------------------------------------------------------------------------- 1 | [bibliography] 2 | .Integración continua 3 | - [[[continuosintegration]]] Paul M Duvall. «Continuos Integration: Improving 4 | Software Quality and Reducing Risk». Addison-Wesley Professional. ISBN 978-0321336385 5 | - [[[continuosdelivery]]] Jez Humble y David Farley. «Continuos Delivery». 6 | Addison-Wesley Professional. ISBN 978-0321601919 7 | - [[[fowlerci-bis]]] Martin Fowler. «Continuous Integration». 8 | http://www.martinfowler.com/articles/continuousIntegration.html 9 | - [[[xpexplained]]] Kent Beck y Cynthia Andres. «Extreme Programming Explained: 10 | Embrace Change». Addison-Wesley Professional. ISBN 978-0321278654 11 | - [[[bleyco]]] Carlos Blé Jurado. «Diseño ágil con TDD». ISBN 978-1445264714. http://dirigidoportests.com/el-libro 12 | - [[[xunit]]] Wikipedia. «Frameworks xUnit». http://en.wikipedia.org/wiki/XUnit 13 | -------------------------------------------------------------------------------- /consejospruebas-biblio.asc: -------------------------------------------------------------------------------- 1 | [bibliography] 2 | .Siete problemas al probar programas 3 | - [[[pseudocode]]] Esteban Manchado Velázquez. «From pseudo-code to code». 4 | http://hcoder.org/2010/08/10/from-pseudo-code-to-code/ 5 | - [[[testing123]]] Esteban Manchado Velázquez. «Software automated testing 123». 6 | http://www.demiurgo.org/charlas/testing-123/ 7 | - [[[fowlerci]]] Martin Fowler. «Continuous Integration». 8 | http://www.martinfowler.com/articles/continuousIntegration.html 9 | - [[[kivaapi]]] API pública de Kiva. http://build.kiva.org/docs/ 10 | - [[[loanmeter]]] Esteban Manchado Velázquez. «World Loanmeter». 11 | https://github.com/emanchado/world-loanmeter 12 | - [[[jasmine]]] Página web de Jasmine, un módulo de pruebas para Javascript. 13 | http://pivotal.github.com/jasmine/ 14 | - [[[pydoubles]]] PyDoubles Test doubles framework. http://www.pydoubles.org/ 15 | -------------------------------------------------------------------------------- /funcionales-biblio.asc: -------------------------------------------------------------------------------- 1 | [bibliography] 2 | .Lecciones de aprender un lenguaje funcional 3 | - [[[onlisp]]] Paul Graham. «On Lisp». Prentice Hall. ISBN 0130305529. 4 | http://www.paulgraham.com/onlisp.html 5 | - [[[landoflisp]]] Conrad Barski. «Land of Lisp». No Starch Press. ISBN 6 | 978-1-59327-281-4. http://landoflisp.com/ 7 | - [[[learnhaskell]]] Miran Lipovača. «Learn You a Haskell for Great Good!». No 8 | Starch Press. ISBN 978-1-59327-283-8. http://learnyouahaskell.com/ 9 | - [[[progscala]]] Dean Wampler y Alex Payne. «Programming Scala». O'Reilly 10 | Media. ISBN 978-0-596-15595-7. http://ofps.oreilly.com/titles/9780596155957/ 11 | - [[[proginscala]]] Martin Odersky, Lex Spoon, y Bill Venners. «Programming in 12 | Scala». Artima. ISBN 9780981531601. http://www.artima.com/pins1ed/ 13 | - [[[jsfuncional]]] Dmitry A. Soshnikov. «JavaScript array "extras" in detail». 14 | http://dev.opera.com/articles/view/javascript-array-extras-in-detail/ 15 | -------------------------------------------------------------------------------- /fix-highlighting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import re 5 | from subprocess import Popen, PIPE 6 | import HTMLParser 7 | 8 | file = sys.argv[1] 9 | lang_sig_re = re.compile(r'\s*<pre class="programlisting">LANGUAGE=(\S+) ') 10 | end_lang_sig_re = re.compile(r'</pre>$') 11 | 12 | f = open(file) 13 | state = 'init' 14 | code_html = "" 15 | lang = "" 16 | for line in f: 17 | m = lang_sig_re.search(line) 18 | if m: 19 | state = 'programlisting' 20 | lang = m.group(1) 21 | code_html = re.sub(lang_sig_re, '', line) 22 | elif state == 'programlisting': 23 | code_html = code_html + line 24 | 25 | if state == 'programlisting' and end_lang_sig_re.search(line): 26 | code_html = re.sub(end_lang_sig_re, '', code_html) 27 | line = "" 28 | state = 'init' 29 | p = Popen(["source-highlight", "-s", lang], stdin=PIPE, stdout=PIPE) 30 | code = HTMLParser.HTMLParser().unescape(unicode(code_html, "utf-8")) 31 | p.stdin.write(code.encode("utf-8")) 32 | p.stdin.close() 33 | print p.stdout.read().replace('<pre', '<pre class="programlisting"') 34 | 35 | if state == 'init': 36 | print line, 37 | 38 | f.close() 39 | -------------------------------------------------------------------------------- /insert_snippets.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RAW_BASE_URL=https://raw.github.com/emanchado/camino-mejor-programador-codigo 4 | PRETTY_BASE_URL=https://github.com/emanchado/camino-mejor-programador-codigo/blob/master 5 | 6 | function normalise() { 7 | snippet=`cat` 8 | 9 | while [[ `echo "$snippet" | grep -v '^ ' | grep -v '^$' | wc -l` == "0" ]]; do 10 | snippet=`echo "$snippet" | sed 's/^ //'` 11 | done 12 | 13 | echo "$snippet" 14 | } 15 | 16 | function retrieve_snippet (){ 17 | url="${RAW_BASE_URL}/${1}" 18 | src="../camino-mejor-programador-codigo/${1}" 19 | first=$2 20 | last=$3 21 | 22 | sed -n -e "${first},${last} p" "${src}" | normalise 23 | #curl --fail -s "$url" | sed -n -e "${first},${last} p" | normalise 24 | #[[ ${PIPESTATUS[0]} != "0" ]] && echo failed to download "$url" && exit -1 25 | } 26 | 27 | #matches: '#SNIPPET "a label" some/url.scala 7 27#' 28 | pattern='^#SNIPPET *"([^"]*)" *([^ ][^ ]*\.([^ .][^ .]*)) *([0-9][0-9]*) *([0-9][0-9]*)#$' 29 | while IFS='' read -r LINE; do 30 | if [[ "$LINE" =~ $pattern ]]; then 31 | label=${BASH_REMATCH[1]} 32 | url=${BASH_REMATCH[2]} 33 | extension=${BASH_REMATCH[3]} 34 | first=${BASH_REMATCH[4]} 35 | last=${BASH_REMATCH[5]} 36 | 37 | echo ".${PRETTY_BASE_URL}/${url}[${label}]" 38 | echo "[source,${extension}]" 39 | echo ----------------------------------------------------------------------------- 40 | retrieve_snippet "${url}" "${first}" "${last}" 41 | echo ----------------------------------------------------------------------------- 42 | else 43 | echo "$LINE" 44 | fi 45 | done 46 | 47 | -------------------------------------------------------------------------------- /tex-listing-title-hack.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Encode; 7 | 8 | my $file = shift @ARGV; 9 | 10 | my $title = "YOU HAVE A BUG, SUCKER"; 11 | 12 | # Turns stupid code snippet captions from: 13 | # {\bf Code caption} 14 | # 15 | # \begin{lstlisting}[... 16 | # to: 17 | # \begin{lstlisting}[...,title={Code caption}] 18 | # 19 | # Moreover, as the TeX conversion drops links from captions for some 20 | # reason, we try to fish them back by grepping the sources (UGH). 21 | 22 | sub find_snippet_url { 23 | my ($title) = @_; 24 | 25 | $title =~ s/\s+\(.*//; # remove the filename between parentheses 26 | for (<../*.asc>) { 27 | open F2, $_; 28 | while (my $line = decode("utf-8", <F2>)) { 29 | # Drop the "_" from the line before trying to match: the 30 | # emphasis marks are dropped from the LaTeX version 31 | $line =~ s/_//g; 32 | if ($line =~ /^\.$title \((.*)\[.*\]\)/) { 33 | return $1; 34 | } 35 | } 36 | close F2; 37 | } 38 | 39 | return; 40 | } 41 | 42 | open F, $file; 43 | while (my $line = <F>) { 44 | if ($line =~ /{\\bf (.+)}/) { 45 | $title = decode("iso-8859-1", $1); 46 | <F>; # Discard the next line, should be empty 47 | } elsif ($title && $line =~ /^\\begin{lstlisting}/) { 48 | # Try to find the URL for this snippet. If found, add a link. 49 | my $url = find_snippet_url($title) // ""; 50 | if ($url) { 51 | $title =~ /(.+) \((.+)\)/; 52 | my ($human_title, $filename) = ($1, $2); 53 | $line =~ s/,\]/,title={$human_title (\\href{$url}{$filename})}]/; 54 | } else { 55 | $line =~ s/,\]/,title={$title}]/; 56 | } 57 | print $line; 58 | $title = undef; 59 | } else { 60 | print $line; 61 | } 62 | } 63 | close F; 64 | -------------------------------------------------------------------------------- /leanpub/convert.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Encode; 7 | use File::Spec; 8 | 9 | my $help_string = "Need a source directory with the ch*.txt files, a destination directory\nand the directory with the HTML \"sources\"!\n"; 10 | my $orig_dir = shift || die $help_string; 11 | my $dest_dir = shift || die $help_string; 12 | my $html_dir = shift || die $help_string; 13 | 14 | opendir D, $orig_dir or die "Can't open directory $orig_dir"; 15 | while (my $file = readdir D) { 16 | next unless $file =~ /^(ch|bi)\d+\.txt$/; 17 | 18 | open F, File::Spec->catfile($orig_dir, $file); 19 | open F1, ">" . File::Spec->catfile($dest_dir, $file); 20 | 21 | # Tweak for syntax highlighting. Files like "highlighting-ch04.txt" 22 | # can be used to force a certain language for the syntax 23 | # highlighting (the first line is the character encoding for the 24 | # output, and each line after that is for one source code block, in 25 | # order). 26 | my @langs; 27 | open FLANGS, "highlighting-$file" and do { 28 | @langs = map { chomp; $_ } <FLANGS>; 29 | close FLANGS; 30 | }; 31 | 32 | my $status = 'header'; 33 | my $source_block_n = 0; 34 | while (my $line = <F>) { 35 | if ($status eq 'header') { 36 | if ($line =~ /^\* \* \*/) { 37 | $status = 'title'; 38 | } 39 | } 40 | elsif ($status eq 'title') { 41 | if ($line =~ /^#/) { 42 | $line =~ s/\#//; 43 | $status = 'print'; 44 | } 45 | print F1 $line; 46 | } 47 | elsif ($status eq 'print') { 48 | if ($line =~ /^\* \* \*/) { 49 | $status = 'footer'; 50 | } 51 | else { 52 | if ($line =~ /^ $/) { 53 | while ($line = <F>) { 54 | if ($line =~ /^_/) { 55 | my $html_file = File::Spec->catfile($html_dir, $file); 56 | $html_file =~ s/\.txt/.html/o; 57 | $source_block_n++; 58 | my $source_code = 59 | decode('utf-8', `./get_sources.pl $html_file $source_block_n`); 60 | $source_code =~ s/^/ /gom; 61 | my $current_lang = $langs[$source_block_n - 1]; 62 | if ($current_lang) { 63 | $source_code = qq({:lang="$current_lang"}) . "\n" . 64 | $source_code; 65 | } 66 | print F1 "\n", encode('utf-8', $source_code); 67 | last; 68 | } 69 | } 70 | } 71 | 72 | # Fix footnotes 73 | $line =~ s/\[\[(\d+)\]\(#ftn.idp\d+\)\]/[^$1]/g; 74 | # Fix bibliography references 75 | $line =~ s/bi01\.html/(#/g; 76 | 77 | # Add ids to bibliography entries 78 | if ($file =~ /^bi/) { 79 | $line =~ s/^\[([^\]]+)\]/{#$1}\n[$1]/; 80 | } 81 | 82 | print F1 $line; 83 | } 84 | } 85 | elsif ($status eq 'footer') { 86 | # We collect any footnotes there might be here 87 | if ($line =~ /^\[\[(\d+)\][^\]]+\] (.+)/) { 88 | my ($ft_number, $ft_text) = ($1, $2); 89 | # Fix bibliography references 90 | $ft_text =~ s/\(bi01\.html#/(#/g; 91 | print F1 "[^$ft_number]: $ft_text\n\n"; 92 | } 93 | } 94 | } 95 | close F1; 96 | close F; 97 | } 98 | closedir D; 99 | -------------------------------------------------------------------------------- /extras/eclipse.asciidoc: -------------------------------------------------------------------------------- 1 | Eclipse 2 | ======= 3 | Yeray Darias Camacho <ydarias@gmail.com> 4 | 5 | Cuando se trabaja con Java, una de las herramientas más famosas y a tener muy en cuenta es Eclipse. Se trata de un entorno de desarrollo integrado (IDE) de código libre y gratuito, una de las razones por la que se ha hecho tan famoso. 6 | 7 | Como cualquier otro IDE tiene muchísimas funciones diferentes y soporta la edición de diferentes lenguajes de programación, aunque demuestra una madurez mucho mayor cuando se trabaja con Java. Debido a que utiliza un sistema de plug-ins para su extensión por terceras partes, siempre es conveniente que echemos un vistazo a las aportaciones de la comunidad, nunca se sabe que nuevas herramientas podemos utilizar. 8 | 9 | Crear un proyecto 10 | ----------------- 11 | 12 | El primer paso será la creación del proyecto, en general siempre lo haremos desde el propio IDE, pero hay ocasiones en que el proyecto ha sido creado previamente y lo debemos importar. 13 | 14 | Desde el entorno 15 | ~~~~~~~~~~~~~~~~ 16 | 17 | De serie, Eclipse permite crear una relativa variedad de proyectos Java, desde un proyecto de escritorio a una aplicación web dinámica, que se pueden ver incrementados en función de los plug-ins que tengamos instalados. Pero generalmente el proceso para crear un proyecto es siempre el mismo. 18 | 19 | File > New > Project 20 | 21 | De aquí en adelante cambia en función del tipo de proyecto, pero siempre estamos orientados por un asistente que nos indica que campos debemos rellenar, y como hacerlo. Se nos solicitará el nombre del proyecto, que carpetas de fuentes y de test queremos usar, ese tipo de detalles que solemos hacer manualmente mediante la creación de ficheros y directorios en una consola. 22 | 23 | Desde el sistema de fichero 24 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | Es muy típico en los últimos años encontrarnos con sistemas de construcción como puede ser Maven o sistemas anteriores como Ant, lo que permite trabajar en consola de forma sencilla. Pero cuando los proyectos crecen es más sencillo seguir trabajando en Eclipse, lo cual es posible mediante la opción de importación. 27 | 28 | File > Import 29 | 30 | El tipo de importación que queramos hacer dependerá de si es un proyecto Maven, es un proyecto que se ha creado anteriormente en otro IDE, o tiene alguna otra estructura específica. Obviamente Eclipse no hace magia y si no disponemos de un plug-in que sepa trabajar con la estructura del fichero, no ganaremos mucho trabajando con Eclipse. 31 | 32 | Atajos de teclado 33 | ----------------- 34 | 35 | Para moverse entre distintos paneles o ficheros 36 | 37 | [width="80%",options="header"] 38 | |====================== 39 | |Atajo de teclado |Significado 40 | |+Shift-Cmd-R+ |Abrir un fichero (recurso) 41 | |+Shift-Cmd-T+ |Abrir un tipo 42 | |+Cmd-E+ |Cambiar entre algunas de las pestañas abiertas 43 | |====================== 44 | 45 | Para ejecutar y depurar código 46 | 47 | [width="80%",options="header"] 48 | |====================== 49 | |Atajo de teclado |Significado 50 | |+Alt-Cmd-X |Menú de ejecución (abre un desplegable con las / 51 | posibles opciones) 52 | |+Shift-Cmd-F11+ |Repetir la última ejecución 53 | |+Cmd-F11+ |Repetir la última depuración 54 | |====================== 55 | 56 | Refactorizaciones 57 | ----------------- 58 | 59 | Una de las razones más importantes por las que se utiliza un IDE es porque permite refactorizar el código rápidamente y simplificando increiblemente el proceso. 60 | 61 | Renombrar 62 | ~~~~~~~~~ 63 | 64 | Los IDEs permiten el renombrado de variables, nombres de clases, paquetes, etc. En Eclipse se puede realizar mediante el uso del ratón y los menús contextuales, pero es más sencillo utilizar un atajo de teclas. 65 | 66 | Alt-Cmd-R 67 | 68 | Introducir variable 69 | ~~~~~~~~~~~~~~~~~~~ 70 | 71 | Alt-Cmd-L 72 | 73 | Introducir método 74 | ~~~~~~~~~~~~~~~~~ 75 | 76 | Si seleccionamos un bloque de código, lo podemos extraer como un método de forma automática, haciendo uso del siguiente atajo de teclado 77 | 78 | Alt-Cmd-M 79 | 80 | Reformatear 81 | ----------- 82 | 83 | Shift-Cmd-F 84 | 85 | Generación de código 86 | -------------------- 87 | 88 | Getters & Setters, comparadores 89 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 90 | 91 | Al principio solemos escribir todo el código manualmente, pero es más productivo no tener que escribir código repetitivo. En un lenguaje como Java, los getters, setters o métodos como equals son muy comunes, para ello podemos hacer uso del siguiente atajo de teclado. 92 | 93 | Alt-Cmd-S 94 | 95 | Uso de templates 96 | ~~~~~~~~~~~~~~~~ 97 | 98 | Hay una función muy interesante en Eclipse que es el uso de plantillas, que permiten generar bloques de código más complejos que posteriormente completaremos con el comportamiento deseado. 99 | 100 | Por ejemplo, se pueden generar métodos de test escribiendo tan solo "test" y el atajo de autocompletado (ctrl-espacio). Esta combinación generará el siguiente bloque. 101 | 102 | @Test 103 | public void testName() throws Exception { 104 | } 105 | 106 | Integración con control de versiones 107 | ------------------------------------ 108 | 109 | Aunque Eclipse posee las herramientas necesarias para realizar control de versiones "Out of the box", solo lo hace con CSV que no es la opción más recomendable actualmente. Por suerte hay una gran cantidad de plugins que permite trabajar con SVN, Mercurial o Git entre otros. 110 | 111 | Haciendo uso de la integración con control de versiones, el propio IDE nos informa de los ficheros que tienen cambios, permite comparar con otras versiones del repositorio o realizar las operaciones más comunes sin necesidad de pasar por la consola del sistema. 112 | 113 | Otros sabores de Eclipse 114 | ------------------------ 115 | 116 | Al tratarse de un software de código abierto, existen una gran variedad de diferentes "sabores" de Eclipse, enfocados a diferentes clases de proyectos o tecnologías. Por ejemplo está Spring Tools Suite más conocido como STS, si sueles utilizar el framework Spring, o MyEclipse que es una versión de pago en la que se incluyen gran cantidad de asistentes para simplificar el proceso de desarrollo, o Aptana Studio que intenta proveer mayor flexibilidad en el trabajo con ficheros HTML, JavaScript y CSS entre otros. 117 | 118 | No tienes que pelearte con nadie para demostrar cual es mejor o si Eclipse es mejor que otros IDEs tan solo debes conocer las ventajas que te aporta si algún día decides utilizarlo. 119 | 120 | Conclusiones 121 | ------------ 122 | 123 | Si eres un gran conocedor del lenguaje o tecnología que estas utilizando, es posible que prefieras ahorrarte la cantidad de recursos que consume un IDE. Pero si como en mi caso, trabajas con sistemas de un tamaño relativamente grande, es muy posible que un IDE como Eclipse te ayude a mejorar enormemente tu productividad. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARTICLE_SOURCE_FILES = funcionales documentacion_activa consejospruebas calidad integracion_continua tdd 2 | SOURCE_FILES = book.asc $(foreach article,$(ARTICLE_SOURCE_FILES),$(article).asc $(article)-biblio.asc) 3 | XSLT_OPTS = --xsltproc-opts="--stringparam chapter.autolabel 0 --stringparam chunk.section.depth 0 --stringparam toc.section.depth 0" 4 | BUILD_ID = $(shell git log -1 --format='Committed %ci %H') 5 | BUILD_LINK = $(shell git log -1 --format='<a href="https://www.assembla.com/code/proyecto-libro/git/changesets/%H">Versión %ci %h</a>') 6 | BUILD_ID_URL = $(shell git log -1 --format='https://www.assembla.com/code/proyecto-libro/git/changesets/%H') 7 | 8 | COMMIT_DATE_TIME = $(shell date --date="`git log -1 --format='%ci'`" --utc '+%Y-%m-%d') 9 | FILENAME = $(shell git log -1 --format='camino_$(COMMIT_DATE_TIME)_%h') 10 | 11 | libro: html epub chunked pdf 12 | 13 | 14 | html: $(FILENAME).html 15 | 16 | epub: $(FILENAME).epub 17 | 18 | chunked: $(FILENAME).chunked/index.html 19 | 20 | pdf: $(FILENAME).pdf 21 | 22 | .PHONY: html epub chunked pdf 23 | 24 | book.asc: book.asc.in Makefile 25 | sed -n '/#ARTICLELIST#/,$$ !p' book.asc.in >book.asc 26 | for i in $(ARTICLE_SOURCE_FILES); do \ 27 | echo "include::$$i.asc[]\n" >>book.asc; \ 28 | done 29 | sed -e '1,/#ARTICLELIST#/d' -e '/#BIBLIOLIST#/d' book.asc.in >>book.asc 30 | for i in $(ARTICLE_SOURCE_FILES); do \ 31 | echo "include::$$i-biblio.asc[]\n" >>book.asc; \ 32 | done 33 | sed -e '1,/#BIBLIOLIST#/d' book.asc.in >>book.asc 34 | sed 's/#BUILDID#/$(BUILD_ID)/' book.asc >book.asc-tmp && mv book.asc-tmp book.asc 35 | 36 | $(FILENAME).html: $(SOURCE_FILES) 37 | asciidoc -b html5 -a toc -a toclevels=1 -a themedir=`pwd`/theme-libro --theme=libro book.asc 38 | # Build id 39 | sed 's|^Last updated.*|$(BUILD_LINK)|' book.html >book.html-tmp && mv book.html-tmp book.html 40 | # Table of contents -> Índice general 41 | sed 's|Table of Contents</div>|Índice general</div>|' book.html >book.html-tmp && mv book.html-tmp book.html 42 | # Fix generation of dashes next to words (with no space in between) 43 | sed -e 's/ --\([^ ->]\)/ \—\1/g' -e 's/\([^<][^ -]\)--\([ ,\.:;)(]\)/\1\—\2/' book.html >book.html-tmp && \ 44 | ./single-html-listing-title-hack.pl book.html-tmp >book.html && \ 45 | rm book.html-tmp 46 | mv book.html $(FILENAME).html 47 | 48 | $(FILENAME).epub: book.xml libro.css 49 | a2x $(XSLT_OPTS) -f epub --stylesheet libro.css book.xml 50 | rm -rf epub-tmp 51 | mkdir epub-tmp && cd epub-tmp && unzip ../book.epub 52 | for i in epub-tmp/OEBPS/ch*.html; do \ 53 | xmllint -format $$i >$$i.tmp; \ 54 | ./fix-highlighting.py $$i.tmp >$$i; \ 55 | ./chunked-html-listing-title-hack.pl $$i >$$i.tmp; \ 56 | mv $$i.tmp $$i; \ 57 | done 58 | ./fix-epub-toc.py epub-tmp/OEBPS/toc.ncx >epub-tmp/toc.ncx && mv epub-tmp/toc.ncx epub-tmp/OEBPS/toc.ncx 59 | ./add-epub-cover.sh epub-tmp/OEBPS/content.opf >epub-tmp/content.opf && mv epub-tmp/content.opf epub-tmp/OEBPS/content.opf 60 | cp cover.html epub-tmp/OEBPS 61 | cp cover.png epub-tmp/OEBPS 62 | cd epub-tmp && rm -f ../book.epub && zip -r ../$(FILENAME).epub * 63 | 64 | $(FILENAME).chunked/index.html: book.xml libro.css 65 | cp book.xml $(FILENAME).xml 66 | a2x $(XSLT_OPTS) -f chunked --stylesheet libro.css $(FILENAME).xml 67 | # Table of contents -> Índice general 68 | sed 's|Table of Contents|Índice general|' $(FILENAME).chunked/index.html >$(FILENAME).chunked/index.html-tmp && mv $(FILENAME).chunked/index.html-tmp $(FILENAME).chunked/index.html 69 | for i in $(FILENAME).chunked/ch*.html; do \ 70 | xmllint -format $$i >$$i.tmp; \ 71 | ./fix-highlighting.py $$i.tmp >$$i; \ 72 | sed -e 's/ --\([^ ->]\)/ \—\1/g' -e 's/\([^<][^ -]\)--\([ ,\.:;)(]\)/\1\—\2/' $$i >$$i.tmp && \ 73 | ./chunked-html-listing-title-hack.pl $$i.tmp >$$i; \ 74 | rm $$i.tmp; \ 75 | done 76 | 77 | book.xml: $(SOURCE_FILES) 78 | asciidoc -b docbook book.asc 79 | sed 's/<programlisting language="\([^"]*\)"[^>]*>/&LANGUAGE=\1 /' book.xml >tmp.xml && mv tmp.xml book.xml 80 | 81 | cover.pdf: cover.png 82 | convert $< $@ 83 | 84 | $(FILENAME).pdf: book.tex cover.pdf 85 | rm -rf tex-tmp 86 | mkdir tex-tmp 87 | cat $< >tex-tmp/book.tex 88 | convert cc-by-sa-big.png tex-tmp/cc-by-sa.pdf 89 | cd tex-tmp && \ 90 | sed -e 's/[áéíóúñ]/_/gi' -e 's/cc-by-sa.png/cc-by-sa.pdf/g' book.tex >tmp.tex && \ 91 | ../tex-listing-title-hack.pl tmp.tex >book.tex && \ 92 | TEXINPUTS=/usr/share/dblatex/latex/style//::/etc/asciidoc/dblatex:/usr/share/dblatex/latex// pdflatex book.tex && \ 93 | TEXINPUTS=/usr/share/dblatex/latex/style//::/etc/asciidoc/dblatex:/usr/share/dblatex/latex// pdflatex book.tex && \ 94 | pdftk C=../cover.pdf B=book.pdf cat C B3-end output book-with-cover.pdf keep_final_id && \ 95 | ../set_pdf_metadata.bash <book-with-cover.pdf >../$(FILENAME).pdf 'El camino a un mejor programador' 'Varios Autores' $(BUILD_ID_URL) 96 | 97 | book.tex: $(SOURCE_FILES) 98 | a2x -a lang=es $(XSLT_OPTS) -f tex book.asc 99 | # Make the table of contents only have the article names 100 | sed 's/^\\setcounter{tocdepth}{[0-9]\+}/\\setcounter{tocdepth}{0}/' book.tex >book.tex-tmp && mv book.tex-tmp book.tex 101 | # Add Javascript and Scala language definition rules for the 102 | # "listings" package. Thanks to Lena Herrmann and Mark/Eivind 103 | # for the tip: 104 | # http://lenaherrmann.net/2010/05/20/javascript-syntax-highlighting-in-the-latex-listings-package 105 | # http://tex.stackexchange.com/questions/47175/scala-support-in-listings-package / http://tihlde.org/~eivindw/ 106 | # Also, set secnumdepth to -1 so 107 | # those pesky "Chapter X" are not show at all 108 | sed 's/\\begin{document}/\\lstdefinelanguage{JavaScript}{keywords={typeof,new,true,false,catch,function,return,null,catch,switch,var,if,in,while,do,else,case,break},ndkeywords={class,export,boolean,throw,implements,import,this},sensitive=false,comment=[l]{\/\/},morecomment=[s]{\/*}{*\/},morestring=[b]'"'"',morestring=[b]"}\n% "define" Scala\n\\lstdefinelanguage{scala}{\nmorekeywords={abstract,case,catch,class,def,%\ndo,else,extends,false,final,finally,%\nfor,if,implicit,import,match,mixin,%\nnew,null,object,override,package,%\nprivate,protected,requires,return,sealed,%\nsuper,this,throw,trait,true,try,%\ntype,val,var,while,with,yield},\notherkeywords={=>,<-,<\\%,<:,>:,\\#,@},\nsensitive=true,\nmorecomment=[l]{\/\/},\nmorecomment=[n]{\/*}{*\/},\nmorestring=[b]",\nmorestring=[b]'"'"',\nmorestring=[b]"""\n}\n\\setcounter{secnumdepth}{-1}\n\\renewcommand\\contentsname{\\'"'"'Indice}\n&/' book.tex >book.tex-tmp && mv book.tex-tmp book.tex 109 | # Fix dashes, also probably only for Spanish 110 | sed -e 's/ -{}-{}\([^ -]\)/ \\textemdash{}\1/g' -e 's/\([^ -]\)-{}-{}\([ ,\.:;)(]\)/\1\\textemdash{}\2/' book.tex >book.tex-tmp && mv book.tex-tmp book.tex 111 | 112 | clean: 113 | rm -rf tex-tmp epub-tmp 114 | rm -f book.tex cover.pdf 115 | rm -rf camino_*.chunked camino_*.pdf camino_*.html camino_*.epub 116 | rm -f camino_*.xml 117 | -------------------------------------------------------------------------------- /libro.css: -------------------------------------------------------------------------------- 1 | span.strong { 2 | font-weight: bold; 3 | } 4 | 5 | body blockquote { 6 | color: #888888; 7 | line-height: 1.5; 8 | margin-top: .75em; 9 | margin-bottom: .75em; 10 | margin-left: 1em; 11 | margin-right: 10%; 12 | border-left: 5px solid #F0F0F0; 13 | padding-left: 1em; 14 | } 15 | blockquote p { 16 | line-height: 1.1; 17 | } 18 | 19 | html body { 20 | margin: 1em 5% 1em 5%; 21 | line-height: 1.2; 22 | font-family: Georgia; 23 | } 24 | 25 | body div { 26 | margin: 0; 27 | } 28 | 29 | h1, h2, h3, h4, h5, h6 30 | { 31 | color: #527bbd; 32 | font-family: tahoma, verdana, sans-serif; 33 | } 34 | 35 | p[title] { 36 | color: #888; 37 | font-family: "Arial", "Helvetica", sans-serif; 38 | margin-bottom: 0.5em; 39 | margin-top: 0.5ex; 40 | font-size: 90%; 41 | } 42 | 43 | p[title] strong { 44 | font-weight: inherit; 45 | } 46 | 47 | div.toc p:first-child, 48 | div.list-of-figures p:first-child, 49 | div.list-of-tables p:first-child, 50 | div.list-of-examples p:first-child, 51 | div.example p.title, 52 | div.sidebar p.title 53 | { 54 | font-weight: bold; 55 | color: #527bbd; 56 | font-family: tahoma, verdana, sans-serif; 57 | margin-bottom: 0.2em; 58 | } 59 | 60 | body h1 { 61 | margin: .0em 0 0 -4%; 62 | line-height: 1.3; 63 | border-bottom: 2px solid silver; 64 | } 65 | 66 | body h2 { 67 | margin: 0.5em 0 0 -4%; 68 | line-height: 1.3; 69 | border-bottom: 2px solid silver; 70 | } 71 | 72 | body h3 { 73 | margin: .8em 0 0 -3%; 74 | line-height: 1.3; 75 | } 76 | 77 | body h4 { 78 | margin: .8em 0 0 -3%; 79 | line-height: 1.3; 80 | } 81 | 82 | body h5 { 83 | margin: .8em 0 0 -2%; 84 | line-height: 1.3; 85 | } 86 | 87 | body h6 { 88 | margin: .8em 0 0 -1%; 89 | line-height: 1.3; 90 | } 91 | 92 | body hr { 93 | border: none; /* Broken on IE6 */ 94 | } 95 | div.footnotes hr { 96 | border: 1px solid silver; 97 | } 98 | 99 | div.navheader th, div.navheader td, div.navfooter td { 100 | font-family: sans-serif; 101 | font-size: 0.9em; 102 | font-weight: bold; 103 | color: #527bbd; 104 | } 105 | div.navheader img, div.navfooter img { 106 | border-style: none; 107 | } 108 | div.navheader a, div.navfooter a { 109 | font-weight: normal; 110 | } 111 | div.navfooter hr { 112 | border: 1px solid silver; 113 | } 114 | 115 | body td { 116 | line-height: 1.2 117 | } 118 | 119 | body th { 120 | line-height: 1.2; 121 | } 122 | 123 | ol { 124 | line-height: 1.2; 125 | } 126 | 127 | ul, body dir, body menu { 128 | line-height: 1.2; 129 | } 130 | 131 | html { 132 | margin: 0; 133 | padding: 0; 134 | } 135 | 136 | body h1, body h2, body h3, body h4, body h5, body h6 { 137 | margin-left: 0 138 | } 139 | 140 | body pre { 141 | margin: 0.5em 10% 0.5em 1em; 142 | color: navy; 143 | font-family: Georgia; 144 | font-size: inherit; 145 | } 146 | 147 | tt.literal, code.literal { 148 | color: navy; 149 | } 150 | 151 | .programlisting, .screen { 152 | margin: 0.5em 10% 0 0; 153 | padding: 0.5em; 154 | background: #F8F8F8; 155 | border: 1px solid #DDDDDD; 156 | border-left: 5px solid #F0F0F0; 157 | } 158 | 159 | div.sidebar { 160 | background: #ffffee; 161 | margin: 1.0em 10% 0.5em 0; 162 | padding: 0.5em 1em; 163 | border: 1px solid silver; 164 | } 165 | div.sidebar * { padding: 0; } 166 | div.sidebar div { margin: 0; } 167 | div.sidebar p.title { 168 | margin-top: 0.5em; 169 | margin-bottom: 0.2em; 170 | } 171 | 172 | div.bibliomixed { 173 | margin: 0.5em 5% 0.5em 1em; 174 | } 175 | 176 | div.glossary dt { 177 | font-weight: bold; 178 | } 179 | div.glossary dd p { 180 | margin-top: 0.2em; 181 | } 182 | 183 | dl { 184 | margin: .8em 0; 185 | line-height: 1.2; 186 | } 187 | 188 | dt { 189 | margin-top: 0.5em; 190 | } 191 | 192 | dt span.term { 193 | font-style: normal; 194 | color: navy; 195 | } 196 | 197 | div.variablelist dd p { 198 | margin-top: 0; 199 | } 200 | 201 | div.itemizedlist li, div.orderedlist li { 202 | margin-left: -0.8em; 203 | margin-top: 0.5em; 204 | } 205 | 206 | ul, ol { 207 | list-style-position: outside; 208 | } 209 | 210 | div.sidebar ul, div.sidebar ol { 211 | margin-left: 2.8em; 212 | } 213 | 214 | div.itemizedlist p.title, 215 | div.orderedlist p.title, 216 | div.variablelist p.title 217 | { 218 | margin-bottom: -0.8em; 219 | } 220 | 221 | div.revhistory table { 222 | border-collapse: collapse; 223 | border: none; 224 | } 225 | div.revhistory th { 226 | border: none; 227 | color: #527bbd; 228 | font-family: tahoma, verdana, sans-serif; 229 | } 230 | div.revhistory td { 231 | border: 1px solid silver; 232 | } 233 | 234 | /* Keep TOC and index lines close together. */ 235 | div.toc dl, div.toc dt, 236 | div.list-of-figures dl, div.list-of-figures dt, 237 | div.list-of-tables dl, div.list-of-tables dt, 238 | div.indexdiv dl, div.indexdiv dt 239 | { 240 | line-height: normal; 241 | margin-top: 0; 242 | margin-bottom: 0; 243 | } 244 | 245 | /* 246 | Table styling does not work because of overriding attributes in 247 | generated HTML. 248 | */ 249 | div.table table, 250 | div.informaltable table 251 | { 252 | margin-left: 0; 253 | margin-right: 5%; 254 | margin-bottom: 0.8em; 255 | } 256 | div.informaltable table 257 | { 258 | margin-top: 0.4em 259 | } 260 | div.table thead, 261 | div.table tfoot, 262 | div.table tbody, 263 | div.informaltable thead, 264 | div.informaltable tfoot, 265 | div.informaltable tbody 266 | { 267 | /* No effect in IE6. */ 268 | border-top: 3px solid #527bbd; 269 | border-bottom: 3px solid #527bbd; 270 | } 271 | div.table thead, div.table tfoot, 272 | div.informaltable thead, div.informaltable tfoot 273 | { 274 | font-weight: bold; 275 | } 276 | 277 | div.mediaobject img { 278 | margin-bottom: 0.8em; 279 | } 280 | div.figure p.title, 281 | div.table p.title 282 | { 283 | margin-top: 1em; 284 | margin-bottom: 0.4em; 285 | } 286 | 287 | div.calloutlist p 288 | { 289 | margin-top: 0em; 290 | margin-bottom: 0.4em; 291 | } 292 | 293 | a img { 294 | border-style: none; 295 | } 296 | 297 | @media print { 298 | div.navheader, div.navfooter { display: none; } 299 | } 300 | 301 | span.aqua { color: aqua; } 302 | span.black { color: black; } 303 | span.blue { color: blue; } 304 | span.fuchsia { color: fuchsia; } 305 | span.gray { color: gray; } 306 | span.green { color: green; } 307 | span.lime { color: lime; } 308 | span.maroon { color: maroon; } 309 | span.navy { color: navy; } 310 | span.olive { color: olive; } 311 | span.purple { color: purple; } 312 | span.red { color: red; } 313 | span.silver { color: silver; } 314 | span.teal { color: teal; } 315 | span.white { color: white; } 316 | span.yellow { color: yellow; } 317 | 318 | span.aqua-background { background: aqua; } 319 | span.black-background { background: black; } 320 | span.blue-background { background: blue; } 321 | span.fuchsia-background { background: fuchsia; } 322 | span.gray-background { background: gray; } 323 | span.green-background { background: green; } 324 | span.lime-background { background: lime; } 325 | span.maroon-background { background: maroon; } 326 | span.navy-background { background: navy; } 327 | span.olive-background { background: olive; } 328 | span.purple-background { background: purple; } 329 | span.red-background { background: red; } 330 | span.silver-background { background: silver; } 331 | span.teal-background { background: teal; } 332 | span.white-background { background: white; } 333 | span.yellow-background { background: yellow; } 334 | 335 | span.big { font-size: 2em; } 336 | span.small { font-size: 0.6em; } 337 | 338 | span.underline { text-decoration: underline; } 339 | span.overline { text-decoration: overline; } 340 | span.line-through { text-decoration: line-through; } 341 | -------------------------------------------------------------------------------- /book.asc.in: -------------------------------------------------------------------------------- 1 | El camino a un mejor programador 2 | ================================ 3 | Esteban Manchado Velázquez, Joaquín Caraballo Moreno, Yeray Darias Camacho 4 | :doctype: book 5 | 6 | :leveloffset: 1 7 | 8 | Licencia 9 | ======== 10 | 11 | Esta obra se distribuye bajo la http://creativecommons.org/licenses/by-sa/3.0/deed.es_ES[licencia Creative Commons Reconocimiento-CompartirIgual 3.0]. Esto significa que usted puede: 12 | 13 | * copiar, distribuir y comunicar públicamente la obra 14 | * remezclar: transformar la obra 15 | * hacer un uso comercial de esta obra 16 | 17 | Bajo las condiciones siguientes: 18 | 19 | * Reconocimiento: debe reconocer los créditos de la obra de la manera especificada por el autor o el licenciador (pero no de una manera que sugiera que tiene su apoyo o apoyan el uso que hace de su obra). 20 | * Compartir bajo la misma licencia: si altera o transforma esta obra, o genera una obra derivada, sólo puede distribuir la obra generada bajo una licencia idéntica a ésta. 21 | 22 | Entendiendo que: 23 | 24 | * Renuncia: alguna de estas condiciones puede no aplicarse si se obtiene el permiso del titular de los derechos de autor 25 | * Dominio público: cuando la obra o alguno de sus elementos se halle en el dominio público según la ley vigente aplicable, esta situación no quedará afectada por la licencia. 26 | * Otros derechos: los derechos siguientes no quedan afectados por la licencia de ninguna manera: 27 | ** Los derechos derivados de usos legítimos u otras limitaciones reconocidas por ley no se ven afectados por lo anterior. 28 | ** Los derechos morales del autor; 29 | ** Derechos que pueden ostentar otras personas sobre la propia obra o su uso, como por ejemplo derechos de imagen o de privacidad. 30 | * Aviso: al reutilizar o distribuir la obra, tiene que dejar bien claro los términos de la licencia de esta obra. 31 | 32 | image:cc-by-sa.png[Creative Common Reconocimiento-CompartirIgual 3.0] 33 | 34 | Agradecimientos 35 | =============== 36 | 37 | A todos aquellos que ayudaron de una manera u otra en la creación de 38 | este libro, con consejos, apoyo o ideas. Las siguientes personas se 39 | merecen una mención especial: 40 | 41 | * Sara Pettersson, por la portada 42 | * Mario Hernández, por el prólogo 43 | * Carlos Ble, por ayudar con la revisión de varios artículos y por sus 44 | ideas y consejos 45 | * Juanje Ojeda, por sus ideas y empuje iniciales 46 | 47 | Prólogo 48 | ======= 49 | 50 | Se cuenta que una vez le preguntaron a Miguel Ángel cómo había procedido para esculpir el David. Su respuesta fue que lo que había hecho era, simplemente, eliminar del bloque de piedra original todo lo que sobraba. Independientemente de la verosimilitud de esta anécdota, podemos encontrar en ella la enseñanza de una actitud a tomar para abordar problemas prácticos de desarrollo en ingeniería. 51 | 52 | Si la pregunta se le hiciese a un escultor que modelase en barro la respuesta equivalente sería, posiblemente, que lo que había hecho era rellenar el espacio disponiendo el barro estrictamente necesario en las zonas adecuadas. Estas dos respuestas se refieren a dos aproximaciones para abordar procesos constructivos: la sustractiva, natural en la escultura en piedra, en la que se eliminan elementos de una solución primigenia hasta que se cumple con las restricciones de la solución definitiva, y la aditiva en la que se aborda el problema partiendo de una solución inicial vacía, añadiendo elementos según ciertos criterios con el fin de conseguir que la solución final cumpla los objetivos de diseño. Estas dos son aproximaciones extrapolables a otras disciplinas y en general, en muchos otros casos, la solución se alcanza con aproximaciones mixtas, en las que se opera con acciones aditivas y sustractivas combinadas. 53 | 54 | El desarrollo de software es una disciplina práctica relativamente nueva que tiene poco más de medio siglo de existencia, pero que conceptualmente es hija de las disciplinas clásicas constructivas englobadas bajo el paraguas de las ingenierías y la arquitectura. De ellas hereda una de las habilidades que necesita un buen informático; la de resolver problemas. Por ella me refiero a la capacidad, en palabras de Allen Downey, el autor de «Think Python. How to Think Like a Computer Scientist», para formular problemas, pensar en soluciones de manera creativa, y expresarlas de forma clara y precisa. A esto se debe probablemente mucha de la genialidad de Miguel Ángel. 55 | 56 | Volviendo a las disciplinas clásicas de la ingeniería y la arquitectura, el objetivo de estas no es el de las ciencias clásicas de carácter analítico, es decir, no intenta entender, explicar y describir algo ya existente como lo hacen la física, la química o la biología, sino más bien sintético, es decir, orientado a desarrollar y construir cosas nuevas que satisfagan un objetivo expresado normalmente por la función definida a priori que debe cumplir el resultado. En este ámbito, el conocimiento acerca de cómo realizar bien las tareas de desarrollo de nuevas soluciones es un trabajo de destilación que se produce en ciclos de vida resumibles en «observación-análisis-propuestas de solución-realización práctica-evaluación de resultados-refinamiento, para alcanzar el nivel de aceptación-corrección para iniciar de nuevo el ciclo o descarte de la propuesta» según corresponda. Estos procesos se realizan iterativamente en el tiempo, constituyendo la fuente del conocimiento práctico del saber hacer de la disciplina y que se terminan expresando como recetas metodológicas y conjuntos de buenas prácticas para ejecutar los proyectos y tareas futuros. Su fin es llevar esos proyectos a buen término en forma de una solución que sea eficiente y efectiva y cumpla las restricciones impuestas al mínimo coste de realización posible medido en horas-hombre. Este proceso es análogo, de nuevo, a los desarrollos de la ingeniería y la arquitectura primigenia, donde los constructores y albañiles desarrollaban su conocimiento y metodologías de forma práctica enfrentándose a retos y problemas nuevos. Ello permitió, por ejemplo, construir las grandes obras de ingeniería civil durante la época de esplendor de Roma, como es el caso de los acueductos, palacios o coliseos, o la gran arquitectura revolucionaria de las catedrales de estilo gótico. Esas construcciones fueron productos pero también retos, que permitieron desarrollar conocimiento y metodologías que se explotaron y depuraron durante siglos. 57 | 58 | La realización de proyectos en disciplinas sintéticas es pues una combinación virtuosa de la concepción y uso de las herramientas adecuadas para el desarrollo, con el conocimiento, las habilidades con esas herramientas y procesos y la experiencia disponible en la mente de quienes los ejecutan, que se manifiesta en la capacidad de resolución de problemas y se expresa como recetas metodológicas y conjuntos de buenas prácticas para llevar a buen fin los proyectos por parte de equipos de personas cualificadas. Una buena ingeniería o una buena arquitectura podrían entenderse en el sentido de Miguel Ángel como las disciplinas que tienen por objetivo el uso adecuado del conjunto de herramientas, conocimiento y habilidades combinados con una metodología que permite construir lo nuevo que se propone cumpliendo los objetivos de diseño, buscando quedarnos con lo estrictamente necesario en sentido constructivo y eliminando lo superfluo. 59 | 60 | ¿Cuál es la diferencia entre el desarrollo de proyectos de programación y el desarrollo de proyectos de ingeniería o arquitectura más o menos convencionales? Pues, conceptualmente el único conjunto de diferencias surge, básicamente, de la naturaleza del objeto de trabajo. En el caso de los últimos, el objeto lo constituyen tangibles físicos, mientras que en el caso del primero, el objeto es fundamentalmente un intangible: todo lo relacionado con la información y sus transformaciones. Además, por la naturaleza de su resultado y al contrario que en otras disciplinas constructivas, el objeto de la ejecución de proyectos software no está sometido a ley física alguna, excepto la que regula la entropía. De hecho, una gran parte de la actividad metodológica en el desarrollo de proyectos está dirigida a luchar contra la degradación y el desorden. Esta situación es la que establece la particularidad de la ejecución de proyectos software como cuerpo disciplinar, y es la que delimita el territorio sobre el que se tienen que desarrollar herramientas, conocimiento y metodologías. 61 | 62 | Este libro pretende ser una aportación a la recopilación y la transmisión del conocimiento y la experiencia de un grupo de buenos y experimentados programadores profesionales en el desarrollo de proyectos de programación. Constituye pues un documento valioso para aquellas personas interesados en el mundo profesional del software. 63 | 64 | Un valor interesante del libro reside en que se dedica a temas que no suelen formar parte del flujo de conocimientos que se manejan usualmente en los procesos de formación básica de los desarrolladores e ingenieros de software, como son los que aparecen en el índice de este documento: el desarrollo de software bajo el paradigma de programación funcional, la documentación activa, la prueba de programas, la calidad del software o los procesos de integración continua. Más bien podemos entenderlo como un texto que intenta servir como transmisor de conocimiento experto desde quienes tienen años de experiencia profesional en el campo y se dirige hacia un amplio espectro de informáticos, tanto experimentados en otros saberes, como noveles en estos temas. En este sentido constituye una obra de valor en los ámbitos que le compete. 65 | 66 | Espero y confío en que esta contribución sirva para los fines que se propusieron sus autores y resulte de utilidad para sus lectores. 67 | 68 | Mario Hernández + 69 | Catedrático de Universidad en Ciencias de la Computación e Inteligencia Artificial + 70 | Las Palmas de Gran Canaria, octubre de 2012 71 | 72 | #ARTICLELIST# 73 | 74 | [bibliography] 75 | Bibliografía 76 | ============ 77 | #BIBLIOLIST# 78 | -------------------------------------------------------------------------------- /theme-libro/libro.css: -------------------------------------------------------------------------------- 1 | /* Shared CSS for AsciiDoc xhtml11 and html5 backends */ 2 | 3 | /* Default font. */ 4 | body { 5 | font-family: Georgia,serif; 6 | } 7 | 8 | /* Title font. */ 9 | h1, h2, h3, h4, h5, h6, 10 | div.title, caption.title, 11 | thead, p.table.header, 12 | #toctitle, 13 | #author, #revnumber, #revdate, #revremark, 14 | #footer { 15 | font-family: Arial,Helvetica,sans-serif; 16 | } 17 | 18 | body { 19 | margin: 1em 5% 1em 5%; 20 | } 21 | 22 | a { 23 | color: blue; 24 | text-decoration: underline; 25 | } 26 | a:visited { 27 | color: fuchsia; 28 | } 29 | 30 | em { 31 | font-style: italic; 32 | } 33 | 34 | strong { 35 | font-weight: bold; 36 | color: #083194; 37 | } 38 | 39 | h1, h2, h3, h4, h5, h6 { 40 | color: #527bbd; 41 | margin-top: 1.2em; 42 | margin-bottom: 0.5em; 43 | line-height: 1.3; 44 | } 45 | 46 | h1, h2, h3 { 47 | border-bottom: 2px solid silver; 48 | } 49 | h2 { 50 | padding-top: 0.5em; 51 | } 52 | h3 { 53 | float: left; 54 | } 55 | h3 + * { 56 | clear: left; 57 | } 58 | h5 { 59 | font-size: 1.0em; 60 | } 61 | 62 | div.sectionbody { 63 | margin-left: 0; 64 | } 65 | 66 | hr { 67 | border: 1px solid silver; 68 | } 69 | 70 | p { 71 | margin-top: 0.5em; 72 | margin-bottom: 0.5em; 73 | } 74 | 75 | ul, ol, li > p { 76 | margin-top: 0; 77 | } 78 | ul > li { color: #aaa; } 79 | ul > li > * { color: black; } 80 | 81 | pre { 82 | padding: 0; 83 | margin: 0; 84 | } 85 | 86 | #author { 87 | color: #527bbd; 88 | font-weight: bold; 89 | font-size: 1.1em; 90 | } 91 | #email { 92 | } 93 | #revnumber, #revdate, #revremark { 94 | } 95 | 96 | #footer { 97 | font-size: small; 98 | border-top: 2px solid silver; 99 | padding-top: 0.5em; 100 | margin-top: 4.0em; 101 | } 102 | #footer-text { 103 | float: left; 104 | padding-bottom: 0.5em; 105 | } 106 | #footer-badges { 107 | float: right; 108 | padding-bottom: 0.5em; 109 | } 110 | 111 | #preamble { 112 | margin-top: 1.5em; 113 | margin-bottom: 1.5em; 114 | } 115 | div.imageblock, div.exampleblock, div.verseblock, 116 | div.quoteblock, div.literalblock, div.listingblock, div.sidebarblock, 117 | div.admonitionblock { 118 | margin-top: 1.0em; 119 | } 120 | div.admonitionblock { 121 | margin-top: 2.0em; 122 | margin-bottom: 2.0em; 123 | margin-right: 10%; 124 | color: #606060; 125 | } 126 | 127 | div.content { /* Block element content. */ 128 | padding: 0; 129 | } 130 | 131 | /* Block element titles. */ 132 | div.title, caption.title { 133 | color: #888888; 134 | font-size: 90%; 135 | font-style: italic; 136 | text-align: left; 137 | margin-top: 0.5ex; 138 | margin-bottom: 1em; 139 | } 140 | div.title + * { 141 | margin-top: 0; 142 | } 143 | 144 | td div.title:first-child { 145 | margin-top: 0.0em; 146 | } 147 | div.content div.title:first-child { 148 | margin-top: 0.0em; 149 | } 150 | div.content + div.title { 151 | margin-top: 0.0em; 152 | } 153 | 154 | div.sidebarblock > div.content { 155 | background: #ffffee; 156 | border: 1px solid #dddddd; 157 | border-left: 4px solid #f0f0f0; 158 | padding: 0.5em; 159 | } 160 | 161 | div.listingblock > div.content { 162 | border: 1px solid #dddddd; 163 | border-left: 5px solid #f0f0f0; 164 | background: #f8f8f8; 165 | padding: 0.5em; 166 | } 167 | 168 | div.quoteblock, div.verseblock { 169 | padding-left: 1.0em; 170 | margin-left: 1.0em; 171 | margin-right: 10%; 172 | border-left: 5px solid #f0f0f0; 173 | color: #888; 174 | } 175 | 176 | div.quoteblock > div.attribution { 177 | padding-top: 0.5em; 178 | text-align: right; 179 | } 180 | 181 | div.verseblock > pre.content { 182 | font-family: inherit; 183 | font-size: inherit; 184 | } 185 | div.verseblock > div.attribution { 186 | padding-top: 0.75em; 187 | text-align: left; 188 | } 189 | /* DEPRECATED: Pre version 8.2.7 verse style literal block. */ 190 | div.verseblock + div.attribution { 191 | text-align: left; 192 | } 193 | 194 | div.admonitionblock .icon { 195 | vertical-align: top; 196 | font-size: 1.1em; 197 | font-weight: bold; 198 | text-decoration: underline; 199 | color: #527bbd; 200 | padding-right: 0.5em; 201 | } 202 | div.admonitionblock td.content { 203 | padding-left: 0.5em; 204 | border-left: 3px solid #dddddd; 205 | } 206 | 207 | div.exampleblock > div.content { 208 | border-left: 3px solid #dddddd; 209 | padding-left: 0.5em; 210 | } 211 | 212 | div.imageblock div.content { padding-left: 0; } 213 | span.image img { border-style: none; } 214 | a.image:visited { color: white; } 215 | 216 | dl { 217 | margin-top: 0.8em; 218 | margin-bottom: 0.8em; 219 | } 220 | dt { 221 | margin-top: 0.5em; 222 | margin-bottom: 0; 223 | font-style: normal; 224 | color: navy; 225 | } 226 | dd > *:first-child { 227 | margin-top: 0.1em; 228 | } 229 | 230 | ul, ol { 231 | list-style-position: outside; 232 | } 233 | ol.arabic { 234 | list-style-type: decimal; 235 | } 236 | ol.loweralpha { 237 | list-style-type: lower-alpha; 238 | } 239 | ol.upperalpha { 240 | list-style-type: upper-alpha; 241 | } 242 | ol.lowerroman { 243 | list-style-type: lower-roman; 244 | } 245 | ol.upperroman { 246 | list-style-type: upper-roman; 247 | } 248 | 249 | div.compact ul, div.compact ol, 250 | div.compact p, div.compact p, 251 | div.compact div, div.compact div { 252 | margin-top: 0.1em; 253 | margin-bottom: 0.1em; 254 | } 255 | 256 | tfoot { 257 | font-weight: bold; 258 | } 259 | td > div.verse { 260 | white-space: pre; 261 | } 262 | 263 | div.hdlist { 264 | margin-top: 0.8em; 265 | margin-bottom: 0.8em; 266 | } 267 | div.hdlist tr { 268 | padding-bottom: 15px; 269 | } 270 | dt.hdlist1.strong, td.hdlist1.strong { 271 | font-weight: bold; 272 | } 273 | td.hdlist1 { 274 | vertical-align: top; 275 | font-style: normal; 276 | padding-right: 0.8em; 277 | color: navy; 278 | } 279 | td.hdlist2 { 280 | vertical-align: top; 281 | } 282 | div.hdlist.compact tr { 283 | margin: 0; 284 | padding-bottom: 0; 285 | } 286 | 287 | .comment { 288 | background: yellow; 289 | } 290 | 291 | .footnote, .footnoteref { 292 | font-size: 0.8em; 293 | } 294 | 295 | span.footnote, span.footnoteref { 296 | vertical-align: super; 297 | } 298 | 299 | #footnotes { 300 | margin: 20px 0 20px 0; 301 | padding: 7px 0 0 0; 302 | } 303 | 304 | #footnotes div.footnote { 305 | margin: 0 0 5px 0; 306 | } 307 | 308 | #footnotes hr { 309 | border: none; 310 | border-top: 1px solid silver; 311 | height: 1px; 312 | text-align: left; 313 | margin-left: 0; 314 | width: 20%; 315 | min-width: 100px; 316 | } 317 | 318 | div.colist td { 319 | padding-right: 0.5em; 320 | padding-bottom: 0.3em; 321 | vertical-align: top; 322 | } 323 | div.colist td img { 324 | margin-top: 0.3em; 325 | } 326 | 327 | @media print { 328 | #footer-badges { display: none; } 329 | } 330 | 331 | #toc { 332 | margin-bottom: 2.5em; 333 | } 334 | 335 | #toctitle { 336 | color: #527bbd; 337 | font-size: 1.1em; 338 | font-weight: bold; 339 | margin-top: 1.0em; 340 | margin-bottom: 0.1em; 341 | } 342 | 343 | div.toclevel0, div.toclevel1, div.toclevel2, div.toclevel3, div.toclevel4 { 344 | margin-top: 0; 345 | margin-bottom: 0; 346 | } 347 | div.toclevel2 { 348 | margin-left: 2em; 349 | font-size: 0.9em; 350 | } 351 | div.toclevel3 { 352 | margin-left: 4em; 353 | font-size: 0.9em; 354 | } 355 | div.toclevel4 { 356 | margin-left: 6em; 357 | font-size: 0.9em; 358 | } 359 | 360 | span.aqua { color: aqua; } 361 | span.black { color: black; } 362 | span.blue { color: blue; } 363 | span.fuchsia { color: fuchsia; } 364 | span.gray { color: gray; } 365 | span.green { color: green; } 366 | span.lime { color: lime; } 367 | span.maroon { color: maroon; } 368 | span.navy { color: navy; } 369 | span.olive { color: olive; } 370 | span.purple { color: purple; } 371 | span.red { color: red; } 372 | span.silver { color: silver; } 373 | span.teal { color: teal; } 374 | span.white { color: white; } 375 | span.yellow { color: yellow; } 376 | 377 | span.aqua-background { background: aqua; } 378 | span.black-background { background: black; } 379 | span.blue-background { background: blue; } 380 | span.fuchsia-background { background: fuchsia; } 381 | span.gray-background { background: gray; } 382 | span.green-background { background: green; } 383 | span.lime-background { background: lime; } 384 | span.maroon-background { background: maroon; } 385 | span.navy-background { background: navy; } 386 | span.olive-background { background: olive; } 387 | span.purple-background { background: purple; } 388 | span.red-background { background: red; } 389 | span.silver-background { background: silver; } 390 | span.teal-background { background: teal; } 391 | span.white-background { background: white; } 392 | span.yellow-background { background: yellow; } 393 | 394 | span.big { font-size: 2em; } 395 | span.small { font-size: 0.6em; } 396 | 397 | span.underline { text-decoration: underline; } 398 | span.overline { text-decoration: overline; } 399 | span.line-through { text-decoration: line-through; } 400 | 401 | div.unbreakable { page-break-inside: avoid; } 402 | 403 | 404 | /* 405 | * xhtml11 specific 406 | * 407 | * */ 408 | 409 | tt { 410 | line-height: 1.2; 411 | font-size: inherit; 412 | color: navy; 413 | } 414 | 415 | div.tableblock { 416 | margin-top: 1.0em; 417 | margin-bottom: 1.5em; 418 | } 419 | div.tableblock > table { 420 | border: 3px solid #527bbd; 421 | } 422 | thead, p.table.header { 423 | font-weight: bold; 424 | color: #527bbd; 425 | } 426 | p.table { 427 | margin-top: 0; 428 | } 429 | /* Because the table frame attribute is overriden by CSS in most browsers. */ 430 | div.tableblock > table[frame="void"] { 431 | border-style: none; 432 | } 433 | div.tableblock > table[frame="hsides"] { 434 | border-left-style: none; 435 | border-right-style: none; 436 | } 437 | div.tableblock > table[frame="vsides"] { 438 | border-top-style: none; 439 | border-bottom-style: none; 440 | } 441 | 442 | 443 | /* 444 | * html5 specific 445 | * 446 | * */ 447 | 448 | .monospaced { 449 | font-family: "Courier New", Courier, monospace; 450 | font-size: inherit; 451 | color: navy; 452 | } 453 | 454 | table.tableblock { 455 | margin-top: 1.0em; 456 | margin-bottom: 1.5em; 457 | } 458 | thead, p.tableblock.header { 459 | font-weight: bold; 460 | color: #527bbd; 461 | } 462 | p.tableblock { 463 | margin-top: 0; 464 | } 465 | table.tableblock { 466 | border-width: 3px; 467 | border-spacing: 0px; 468 | border-style: solid; 469 | border-color: #527bbd; 470 | border-collapse: collapse; 471 | } 472 | th.tableblock, td.tableblock { 473 | border-width: 1px; 474 | padding: 4px; 475 | border-style: solid; 476 | border-color: #527bbd; 477 | } 478 | 479 | table.tableblock.frame-topbot { 480 | border-left-style: hidden; 481 | border-right-style: hidden; 482 | } 483 | table.tableblock.frame-sides { 484 | border-top-style: hidden; 485 | border-bottom-style: hidden; 486 | } 487 | table.tableblock.frame-none { 488 | border-style: hidden; 489 | } 490 | 491 | th.tableblock.halign-left, td.tableblock.halign-left { 492 | text-align: left; 493 | } 494 | th.tableblock.halign-center, td.tableblock.halign-center { 495 | text-align: center; 496 | } 497 | th.tableblock.halign-right, td.tableblock.halign-right { 498 | text-align: right; 499 | } 500 | 501 | th.tableblock.valign-top, td.tableblock.valign-top { 502 | vertical-align: top; 503 | } 504 | th.tableblock.valign-middle, td.tableblock.valign-middle { 505 | vertical-align: middle; 506 | } 507 | th.tableblock.valign-bottom, td.tableblock.valign-bottom { 508 | vertical-align: bottom; 509 | } 510 | 511 | 512 | /* 513 | * manpage specific 514 | * 515 | * */ 516 | 517 | body.manpage h1 { 518 | padding-top: 0.5em; 519 | padding-bottom: 0.5em; 520 | border-top: 2px solid silver; 521 | border-bottom: 2px solid silver; 522 | } 523 | body.manpage h2 { 524 | border-style: none; 525 | } 526 | body.manpage div.sectionbody { 527 | margin-left: 3em; 528 | } 529 | 530 | @media print { 531 | body.manpage div#toc { display: none; } 532 | } 533 | -------------------------------------------------------------------------------- /documentacion_activa.asc.in: -------------------------------------------------------------------------------- 1 | Documentación activa 2 | ==================== 3 | Joaquín Caraballo 4 | 5 | Si en un extremo están los que definen el problema a resolver y en el otro los que lo resuelven, ¿cómo podemos asegurarnos de que todos estamos intentando resolver el mismo problema? Y, si aceptamos que la definición va a necesitar ser enriquecida, corregida, matizada, e incluso que el problema a resolver va a cambiar radicalmente durante la vida del proyecto, ¿cómo mantenemos una misma definición para todos los interesados, y más importante, cómo aseguramos que nuestro programa resuelve el problema? 6 | 7 | 8 | Las pruebas funcionales 9 | ----------------------- 10 | 11 | Para tener una cierta confianza en que la aplicación resuelve un cierto problema se llevan a cabo pruebas funcionales; estas resuelven un ejemplo concreto y representativo simulando las acciones del usuario y verificando que la reacción de la aplicación es la esperada. 12 | 13 | Las pruebas funcionales han de mantenerse al día y evolucionar con los cambios de la aplicación y sus requisitos. Otro de los retos es que sean correctas, es decir, que el comportamiento que estén verificando sea realmente el que se requiere de la aplicación. 14 | 15 | Es crucial que las pruebas funcionales estén automatizadas; lo típico es escribirlas en el mismo lenguaje de programación de la aplicación. Esto nos permite ejecutar un gran número de ellas de forma sistemática y tras cada cambio, con lo que también nos protegerán de que algo que funcionaba deje de hacerlo, es decir, de posibles _regresiones_. 16 | 17 | 18 | neverread 19 | ~~~~~~~~~ 20 | 21 | Supongamos que tenemos un cliente con falta de tiempo para leer artículos de Internet. 22 | 23 | ____________________________________________________________________ 24 | -Tantos artículos interesantes, no tengo tiempo para leerlos, pero tampoco puedo ignorarlos. Me gustaría tener una aplicación en la que copiar la dirección del artículo que me interese. 25 | 26 | -¡Ah!, ¿quieres una aplicación que te permita almacenar enlaces a artículos? La aplicación mantendría una lista con los artículos almacenados, a la que luego podrías acceder cuando tengas tiempo y leer los artículos... 27 | 28 | -No, no, si yo lo que quiero es que los enlaces a artículos desaparezcan, si tuviera tiempo para leerlos después usaría instapaper, hombre. Estaría bien que tuviese una lista de artículos por leer siempre vacía, me daría una sensación genial de tenerlo todo bajo control. 29 | 30 | -Er... vale. 31 | ____________________________________________________________________ 32 | 33 | De la conversación hemos conseguido extraer un criterio de aceptación: 34 | ____ 35 | Por mucho que añadamos artículos, estos no se añadirán a una lista de artículos por leer. 36 | ____ 37 | 38 | La prueba funcional correspondiente, verificará que la aplicación cumple el criterio para un ejemplo concreto: 39 | ____ 40 | Cuando añadimos el artículo `art.culo/interesante.html`, la lista de artículos por leer permanece vacía. 41 | ____ 42 | 43 | Las pruebas funcionales, si bien no tienen necesariamente que serlo, se llevan a cabo con frecuencia como pruebas de punta a punta _(end to end)_ (<<goos>> pág 10), es decir, pruebas en las que se ejercita la aplicación en su conjunto y desde afuera, simulando las acciones del usuario y de los sistemas colaboradores, y evaluando la corrección según la perciben usuario y colaboradores. 44 | 45 | Hemos decidido resolver el problema propuesto con una aplicación web, _neverread_. Implementaremos en Java una prueba funcional de punta a punta que verifique el criterio de aceptación con una prueba _junit_ que va a iniciar la aplicación y a continuación ejercitarla y evaluarla. Para simular la interacción de un usuario a través de un navegador web utilizaremos Selenium/WebDriver/HtmlUnit: 46 | 47 | #SNIPPET "purejava/ListStaysEmptyTest.java" active-documentation/src/test-acceptance/purejava/ListStaysEmptyTest.java 15 40# 48 | 49 | Documentación activa 50 | ------------------- 51 | 52 | Si bien podríamos considerar que nuestra prueba funcional en Java es la documentación principal donde se registran los criterios de aceptación; dependiendo de quién vaya a consumirla, esto puede o no ser una opción. Además, conseguir reemplazar la legibilidad de un párrafo en español con una prueba en el lenguaje de programación es todo un reto, particularmente si el lenguaje de programación es tan prolijo como Java. 53 | 54 | En consecuencia, en la mayoría de los proyectos es necesario documentar los requisitos de manera más textual. 55 | 56 | Una forma de intentar obtener lo mejor de las dos alternativas es conectar la documentación a la aplicación de tal forma que en todo momento sea automático y evidente verificar el cumplimiento de cada uno de los requisitos expresados, lo que nos ayudará a mantener la documentación sincronizada con la aplicación y con el cliente. 57 | 58 | La documentación, junto con la capacidad de procesarla para su verificación, servirá así de batería de pruebas funcionales; si ejecutamos esta batería con frecuencia (idealmente con cada cambio) y la mantenemos veraz, estaremos garantizando el correcto funcionamiento de la aplicación... para la definición especificada. 59 | 60 | 61 | Concordion 62 | ---------- 63 | 64 | Concordion es una de las herramientas que nos pueden ayudar a conectar la documentación con la aplicación; en Concordion los criterios se escriben en HTML al que se añaden algunas marcas que comienzan con `concordion:` 65 | 66 | #SNIPPET "concordion/v1/ListStaysEmpty.html" active-documentation/src/test-acceptance/concordion/v1/ListStaysEmpty.html 8 21# 67 | 68 | Vemos que hemos expresado el ejemplo en forma de condición y consecuencia que se debe verificar. Los atributos `concordion:set` y `concordion:assertEquals` conectan el documento al método de la clase de respaldo, escrita en Java, `String articleListAfterAdding(String url)`, que se encargará de hacer lo que dice el texto. 69 | 70 | #SNIPPET "purejava/ListStaysEmptyTest.java" active-documentation/src/test-acceptance/concordion/v1/ListStaysEmptyTest.java 15 38# 71 | 72 | Al ejecutar `ListaPermaneceVaciaTest`, Concordion generará un documento HTML con el texto anterior en el que se indicará si se cumple la aserción resaltándola en verde o rojo. 73 | 74 | 75 | Paso a paso 76 | ~~~~~~~~~~~ 77 | 78 | Veamos qué es lo que está pasando aquí. Hemos escrito el criterio de aceptación en `ListaPermaneceVacia.html`. Acompañando al HTML hemos escrito una clase en Java que extiende una clase de infrastructura de Concordion: `class ListaPermaneceVaciaTest extends ConcordionTestCase`. 79 | 80 | Cuando ejecutamos `ListaPermaneceVaciaTest`: 81 | 82 | . Concordion procesa el HTML. 83 | . Concordion detecta la marca `concordion:set="#url"` y guarda el contenido de esa marca HTML (en este caso, «art.culo/interesante.html») en la variable de Concordion `#url`. 84 | . Concordion detecta la marca `concordion:assertEquals="articleListAfterAdding(#url)"`, por lo que busca en la clase de acompañamiento un método denominado `articleListAfterAdding` y lo ejecuta, pasándole el contenido de `#url` como parámetro. 85 | . El método `articleListAfterAdding` simula la acción de un usuario que introduce `url` y obtiene la lista de artículos resultante. 86 | . Mediante `convertListOfArticlesToString`, transformamos la lista producida por WebDriver en una representación textual que pueda ser comparada con el texto del HTML. Hemos decidido que la representación textual de una lista vacía sea «vacía». 87 | . El método `articleListAfterAdding` retorna, devolviendo una cadena (en este caso «vacía») que es comparada con el contenido de la marca HTML en el que se encontró `concordion:assertEquals`. 88 | . Concordion termina de procesar el documento HTML y genera otro HTML en el que el texto que tiene la marca `concordion:assertEquals` está resaltado en verde, para indicar que la aserción se cumple. 89 | 90 | 91 | Manteniendo el nivel de abstracción apropiado 92 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 93 | 94 | Es importante esforzarse en describir el funcionamiento de la aplicación en términos del dominio. Por ejemplo, podríamos haber caído en la tentación de escribir el ejemplo como _Cuando el usuario entra una cadena en la caja de texto y pulsa enter, la lista de artículos está vacía_. Sin embargo, eso sería perjudicial porque nos alejaría de la persona que define lo que debe hacer la aplicación y resultaría más _frágil_, es decir, en cuanto decidiéramos cambiar la implementación, por ejemplo, supongamos que las direcciones se introducen arrastrándolas a una zona de la aplicación, tendríamos que reescribir el documento. 95 | 96 | A poco que la documentación activa crezca, las clases de respaldo van a necesitar una cantidad importante de código. Algunas abstracciones pueden ayudarnos reducir la repetición y la fragilidad de las clases de respaldo. 97 | 98 | Podemos hacer que la clase de respaldo sólo _hable_ en el lenguaje del dominio, para lo cual hemos de desarrollar un lenguaje dedicado, con lo que el método quedaría algo así: 99 | 100 | #SNIPPET "concordion/v2_appdriver/ListStaysEmptyTest.java" active-documentation/src/test-acceptance/concordion/v2_appdriver/ListStaysEmptyTest.java 18 21# 101 | 102 | Otra posibilidad es abstraer la página web en términos de los elementos del entorno gráfico, es decir, que hable de elementos visuales de la página. 103 | 104 | #SNIPPET "concordion/v3_pagedriver/ListaStaysEmptyTest.java" active-documentation/src/test-acceptance/concordion/v3_pagedriver/ListStaysEmptyTest.java 18 23# 105 | 106 | La capa de abstracción en términos de lenguaje del dominio es la opción más _pura_, pero dependiendo del proyecto podremos preferir una capa que se exprese en términos gráficos o ambas, dependiendo de la complejidad del proyecto y de cuán involucrado esté el cliente en los detalles gráficos. 107 | 108 | 109 | Pruebas asíncronas 110 | ------------------ 111 | 112 | En las secciones anteriores nos hemos permitido hacer un poco _trampas_ que deberíamos descubrir antes de cerrar el artículo. Supongamos que el desarrollador, al escribir el código de la aplicación comete una equivocación por no entender debidamente lo que necesita el cliente; decide hacer una aplicación que _añade_ los artículos a una lista de artículos a leer. Nuestras pruebas funcionales deberían detectar este error marcando la aserción en rojo. Sin embargo, nos encontramos con que la pruebas pasan. 113 | 114 | Evidentemente, nuestras pruebas funcionales no son correctas, esto se debe a que estamos verificando que el estado de la lista de artículos es el mismo después de entrar el nuevo artículo que antes de entrarlo, y la prueba verifica la condición antes de que la aplicación tenga tiempo de añadir erróneamente artículos a la lista. 115 | 116 | Probar sistemas asíncronos es lo suficientemente complejo como para justificar un artículo en sí mismo, pero si enumeramos algunas de las opciones, de más rápidas y sencillas de implementar a menos, tenemos: 117 | 118 | . Probamos sólo la parte síncrona del sistema. Esto hace las pruebas más sencillas y rápidas a costa de reducir el alcance. 119 | . Introducimos puntos de sincronización. Volveremos a este en un segundo. 120 | . Verificamos periódicamente la aserción hasta que se cumpla o se agote el tiempo de espera. En esta opción es crucial ajustar la duración, si esperamos demasiado las pruebas tardarán demasiado innecesariamente, si esperamos demasiado poco tendremos falsos negativos. 121 | 122 | En nuestro ejemplo sabemos que, cada vez que la aplicación responde a la entrada de un nuevo artículo, lo último que hace es borrar la caja de texto. Por lo tanto, podemos utilizar este evento como punto de sincronización, es decir, antes de verificar que la lista permanece vacía esperaremos a que la caja se haya borrado. 123 | 124 | 125 | #SNIPPET "concordion/v5_with_synchronisation/tools/NeverReadDriver.java" active-documentation/src/test-acceptance/concordion/v5_with_synchronisation/tools/NeverReadDriver.java 25 38# 126 | 127 | 128 | Conclusión 129 | ---------- 130 | 131 | La _documentación activa_ es una forma de probar funcionalmente un programa en el que cada criterio de aceptación se enuncia con un texto que se enlaza a la ejecución de código que verifica el criterio. Al ejecutarla, se produce un resultado que indica, de forma legible para los expertos del dominio, qué criterios de aceptación cumple el programa y qué criterios no cumple. Como casi todo, su uso debería adaptarse a la composición del equipo y la complejidad del proyecto. 132 | -------------------------------------------------------------------------------- /extras/herramientas.asciidoc: -------------------------------------------------------------------------------- 1 | Aprende tu editor a fondo 2 | ========================= 3 | Esteban Manchado_Velázquez <emanchado@demiurgo.org> 4 | 5 | Conocer las herramientas es importante. Este artículo se centrará en los 6 | aspectos más importantes de aprender a usar un editor de texto bien, dando 7 | ejemplos para los dos grandes «clásicos» de edición de texto para 8 | programadores: VIM y Emacs. 9 | 10 | 11 | «El mejor editor» 12 | ----------------- 13 | Siempre ha habido y habrá discusiones sobre qué herramientas son mejores o 14 | peores. No hay respuesta única, cada persona tiene un estilo y unas necesidades 15 | diferentes. Lo importante no es encontrar «la mejor», sino escoger una buena 16 | herramienta y aprenderla bien. 17 | 18 | Dos editores populares, especialmente en el mundo Unix, son VIM y Emacs. El 19 | primero es pequeño y está disponible en cualquier instalación de Unix (si no 20 | VIM, al menos algún otro clon de vi). Su punto fuerte es ser un editor pequeño, 21 | rápido y ágil. Cuesta un poco aprenderlo y acostumbrarse a sus diferentes 22 | modos, pero una vez se aprende se puede usar en cualquier sitio. 23 | 24 | Por el contrario, Emacs es bastante grande, muy completo y lleno de 25 | extensiones. Tarda más en arrancar, pero es prácticamente un sistema operativo. 26 | Sus usuarios más devotos lo hacen prácticamente todo desde Emacs: editar texto 27 | (incluso ficheros en otras máquinas), leer el correo, navegar páginas web, 28 | escribir planes, mapas mentales, gestionar listas de tareas, etc. Todo esto se 29 | consigue mediante los llamados «modos» de Emacs, que implementan extensiones, 30 | adaptaciones y mejoras para diferentes actividades o tipos de fichero a editar. 31 | El punto fuerte de Emacs es, sin duda, la programabilidad y las extensiones que 32 | sus usuarios escriben y comparten. No es demasiado cómodo de usar con la 33 | configuración de paquete, por lo que los usuarios tienden a personalizarlo 34 | _mucho_. Esto hace que, una vez se aprende, uno sabe usar la propia 35 | configuración, pero no necesariamente el Emacs configurado por otra persona. 36 | 37 | Cuando empezamos a usar y aprender VIM o Emacs, lo más probable es que tengamos 38 | que cambiar ligeramente nuestras costumbres y adaptarnos a ellos. Esto no es 39 | necesariamente malo, pero conocer las diferentes opciones y posibilidades de 40 | personalización también es buena idea. 41 | 42 | 43 | Movimiento 44 | ---------- 45 | Como programadores, lo más probable es que pasemos una buena parte del día en 46 | nuestro editor. Por tanto, es importante que nuestras manos estén lo más 47 | relajadas posible. Evitar el uso constante del ratón y mantener las manos en 48 | posición de escribir (es decir, evitar el uso de las flechas de movimiento) nos 49 | ayudará a mantener las manos relajadas y a evitar problemas como el túnel 50 | carpiano. Afortunadamente, tanto VIM como Emacs nos permiten movernos sin 51 | necesidad de acudir a las flechas de movimiento. En el caso de VIM, podemos 52 | movernos con las teclas +h+, +j+, +k+, +l+ (izquierda, abajo, arriba y derecha 53 | respectivamente); en el caso de Emacs, con +C-b+, +C-f+, +C-n+ y +C-p+ (por 54 | _backwards_, _forward_, _next_ y _previous_; nótese que, en Emacs, +Ctrl-X+ se 55 | representa como +C-x+, y +Alt-X+ como +M-x+). 56 | 57 | Pero moverse línea a línea, o caracter a caracter, es bastante lento. Por 58 | tanto, aprender a moverse de manera más efectiva es algo de lo que nos podremos 59 | aprovechar todos los días. Y, de nuevo, tanto VIM como Emacs nos ofrecen varias 60 | posibilidades. Entre ellas: 61 | 62 | .Órdenes comunes de movimiento de VIM 63 | [width="80%",options="header"] 64 | |====================== 65 | |Orden de VIM |Atajo de Emacs |Significado 66 | |+w+ / +b+ |+M-f+ / +M-b+ |Palabra adelante / palabra atrás 67 | |+{+ / +}+ |+M-{+ / +M-}+ |Bloque anterior / bloque siguiente 68 | |+Ctrl-F+ / +Ctrl-B+ |+C-v+ / +M-v+ |Página siguiente / página anterior 69 | |+H+ / +M+ / +L+ |+M-r+ (cambia entre las tres) |Parte alta / media / baja de la pantalla 70 | |+f<letra>+ / +F<letra>+ |_No aplicable_ |Ir a la siguiente <letra> de la línea / ir a la anterior <letra> de la línea 71 | |====================== 72 | 73 | Otro consejo, especialmente para Emacs pero aplicable en cualquier caso, es 74 | reasignar la tecla «Bloq. Mayús» como una tercera tecla Control. Esta 75 | configuración hará mucho más cómodo y rápido el usar atajos de teclado con la 76 | tecla «Control», lo cual notaremos a la larga en nuestras manos. Sobre todo con 77 | Emacs. 78 | 79 | 80 | Búsquedas y sustituciones 81 | ------------------------- 82 | Las búsquedas y sustituciones son probablemente las características más usadas 83 | de un editor de texto, después de las órdenes de movimiento. De hecho, en Emacs 84 | la búsqueda se usa con frecuencia para moverse por el texto. En VIM hay varias 85 | órdenes de búsqueda, incluyendo buscar la palabra debajo del cursor (con +*+ o 86 | +#+, dependiendo de si buscamos hacia abajo o hacia arriba). En Emacs tenemos 87 | las búsquedas normales en +C-s+ y +C-r+, y varias funciones de búsqueda y 88 | sustitución, incluyendo +replace-regexp+ y +isearch-forward-at-point+. 89 | 90 | Mención aparte merecen las expresiones regulares, muy útiles a la hora de hacer 91 | tanto búsquedas como sustituciones. VIM, Emacs y muchos otros programas 92 | entienden expresiones regulares, lo que junto a su versatilidad las convierte 93 | en una de las herramientas básicas del programador. Por tanto, vale la pena 94 | aprenderlas suficientemente bien como para poder hacer la gran mayoría de las 95 | sustituciones sin tener que consultar el manual. Emacs en particular tiene una 96 | función muy útil para aprender y experimentar con expresiones regulares: la 97 | función +re-builder+. 98 | 99 | Por último, siempre es útil poder encontrar rápidamente la definición de cierta 100 | función, especialmente si no lo conocemos bien o es grande y complejo. Algunos 101 | editores tienen este tipo de navegación integrada. VIM y Emacs permiten este 102 | tipo de navegación con la ayuda de la herramienta externa «ctags». 103 | 104 | 105 | 106 | Sangrado 107 | -------- 108 | Una de las características más importantes del formato de nuestro código es el 109 | sangrado. Cualquier editor moderno puede ahorrarnos gran parte del trabajo 110 | de mantener el sangrado correcto. Incluyendo a VIM y Emacs, como podríamos 111 | esperar. 112 | 113 | Para sangrar el código que escribimos automáticamente, VIM ofrece, entre otras, 114 | las opciones +formatoptions+ y +smartindent+. Emacs ofrece diferentes «modos» 115 | para diferentes lenguajes, que se encargan de definir cómo se debería sangrar 116 | cada lenguaje. También es importante tener la posibilidad de reformatear código 117 | ya escrito, operación que podemos hacer con la orden +=+ en VIM y la función 118 | +indent-region+ en Emacs. 119 | 120 | Cuando hablamos de sangrado, la mayoría de los programadores prefiere usar 121 | espacios. Naturalmente, es más cómodo pulsar la tecla Tab que pulsar varias 122 | veces la tecla espaciadora, así que la mayoría de los editores nos permite 123 | pulsar la tecla Tab para sangrar (pero manteniendo espacios para el sangrado). 124 | En VIM usamos la opción +expandtab+; en Emacs, +(setq indent-tabs-mode nil)+. 125 | 126 | Y para los casos en los que necesitemos formatear textos y comentarios (para 127 | que se adapten automáticamente a cierto número de caracteres por línea, digamos 128 | 80), VIM nos ofrece la orden +gq+ y Emacs el atajo +M-q+. 129 | 130 | 131 | Tipos de ficheros 132 | ----------------- 133 | Es evidente que tratamos distintos tipos de ficheros de distinta forma: 134 | necesitan unas reglas distintas para el resaltado de sintaxis, para el sangrado 135 | automático, para detectar qué es un comentario, quizás para definir macros, 136 | atajos de teclado especiales o plantillas, etc. La mayoría de los editores 137 | actuales permite, hasta cierto punto, adaptar el entorno a los diferentes tipos 138 | de ficheros. 139 | 140 | Éste es, posiblemente, uno de los puntos más fuertes de Emacs. En Emacs tenemos 141 | los modos, y en VIM tenemos los _filetypes_. La idea de los modos de Emacs 142 | es cambiar el editor para convertirlo en un editor especializado para el tipo 143 | de fichero actual: cada modo tiene su propia configuración, atajos de teclado y 144 | manual, y se aprende más o menos por separado. Una gran parte de la potencia de 145 | Emacs se basa en esto, y la red está llena de modos para todo tipo de tareas, 146 | como Org mode (para planificar, gestionar tareas y proyectos, etc), nXML mode 147 | (para editar y validar XML) o rainbow-mode (para colorear referencias a colores 148 | como #a00 con el color que representan). 149 | 150 | En VIM, la personalización es generalmente mucho menor, y se basa más en 151 | adaptar el sangrado y el resaltado de sintaxis. Sin embargo, nos permite añadir 152 | nuestra propia personalización para cada tipo de fichero, así que siempre 153 | podemos adaptar las opciones activas o cualquier otro aspecto de VIM que 154 | queramos. 155 | 156 | 157 | Macros y atajos de teclado 158 | -------------------------- 159 | La programabilidad de un editor es otro aspecto importante que resulta útil 160 | aprender. No necesariamente la programabilidad «pesada» de escribir o modificar 161 | extensiones para el editor, sino la programabilidad de pequeñas macros y atajos 162 | de teclado. 163 | 164 | En VIM, podemos grabar macros con la orden +q+. Esta orden necesita un 165 | parámetro, que será una letra donde guardaremos la macro. Así, si pulsamos 166 | +qa+, empezaremos a grabar una macro en «a». Para terminar, pulsamos +q+, y 167 | para ejecutar una macro, pulsamos +@+ seguida de la letra que queramos, o +@@+ 168 | para repetir la última macro _ejecutada_. 169 | 170 | En Emacs, lo normal es hacer una de dos cosas: grabar pequeñas macros en la 171 | sesión, o escribir funciones en Emacs Lisp para hacer pequeñas extensiones de 172 | nuestro editor. Además, las primeras se pueden convertir en las segundas con la 173 | ayuda de las funciones +kmacro-name-last-macro+ y +insert-kbd-macro+. Para 174 | grabar una macro en Emacs, pulsamos +F3+ (o +C-x (+) y para terminar de grabar 175 | pulsamos +F4+ (o +C-x )+). Para ejecutar la última macro grabada, pulsamos +C-x 176 | e+. 177 | 178 | Conclusiones 179 | ------------ 180 | Aunque pueda parecer exagerado, herramientas aparentemente simples como un 181 | editor de texto son suficientemente complicadas como para que haya muchas, muy 182 | diferentes y prácticamente imposibles de comparar. Por tanto, no se puede decir 183 | que haya un «mejor editor»: depende de para qué lo usemos, cómo lo usemos, lo 184 | que nos resulte personalmente más cómodo y cuánto hayamos aprendido sobre él. 185 | 186 | Pero la conclusión más importante es que un buen profesional conoce sus 187 | herramientas y las aprende a usar bien. Dedicar tiempo a aprender las 188 | herramientas de trabajo ahorra tiempo a la larga. 189 | 190 | 191 | [bibliography] 192 | Bibliografía 193 | ------------ 194 | - [[[viunixworld]]] Walter Alan Zintz 'The Vi/Ex Editor'. 195 | http://www.networkcomputing.com/unixworld/tutorial/009/009.html 196 | - [[[emacswiki]]] El Wiki de Emacs http://emacswiki.org/ 197 | - [[[boostvim]]] Vincent Driessen 'How I boosted my Vim' 198 | http://nvie.com/posts/how-i-boosted-my-vim/ 199 | - [[[effectiveemacs]]] Steve Yegge 'Effective Emacs' 200 | http://sites.google.com/site/steveyegge2/effective-emacs 201 | - [[[vimcasts]]] 'Vimcasts' 202 | http://vimcasts.org/ 203 | - [[[emacsrocks]]] 'Emacs rocks' 204 | http://emacsrocks.com/ 205 | - [[[masteringemacs]]] 'Mastering Emacs' 206 | http://www.masteringemacs.org 207 | - [[[regularexpressions]]] Jeffrey E.F. Friedl 'Mastering Regular Expressions' 208 | O'Reilly Media. ISBN 0-596-00289-0 209 | http://shop.oreilly.com/product/9780596002893.do 210 | - [[[emacsrebuilder]]] 're-builder: the Interactive regexp builder' 211 | http://www.masteringemacs.org/articles/2011/04/12/re-builder-interactive-regexp-builder/ 212 | - [[[ctags]]] Wikipedia 'Ctags' http://en.wikipedia.org/wiki/Ctags 213 | - [[[vimscripts]]] Vim Scripts http://www.vim.org/scripts/ 214 | - [[[emacsorgmode]]] Org mode para Emacs http://orgmode.org/ 215 | - [[[emacsnxmlmode]]] nXML mode para Emacs 216 | http://www.thaiopensource.com/nxml-mode/ 217 | - [[[emacsrainbowmode]]] Rainbow mode para Emacs 218 | http://julien.danjou.info/rainbow-mode 219 | - [[[introelisp]]] Christian Johansen 'An introduction to Emacs Lisp'. 220 | http://cjohansen.no/an-introduction-to-elisp 221 | -------------------------------------------------------------------------------- /calidad.asc: -------------------------------------------------------------------------------- 1 | Calidad en software 2 | =================== 3 | _Esteban Manchado Velázquez_ 4 | 5 | «Calidad» es una palabra muy usada pero un concepto bastante escurridizo, al 6 | menos a la hora de encontrar maneras fiables de mejorarla o simplemente 7 | mantenerla una vez hemos llegado a un nivel aceptable. Este artículo explorará 8 | qué es la calidad y cómo mejorarla en nuestros proyectos. 9 | 10 | Significado 11 | ----------- 12 | El primer problema de la calidad es definirla. Hay muchas definiciones, pero mi 13 | preferida en el contexto de la ingeniería de software es «adecuación al uso». 14 | Uno de los problemas de la definición es que es muy vaga pero, paradójicamente, 15 | el propio hecho de ser tan vaga es lo que la hace útil. La calidad es un asunto 16 | muy complejo, por lo que simplificarlo, lejos de ayudarnos a entender, sólo nos 17 | dará la ilusión de que lo entendemos. Y la ilusión de entendimiento es muy 18 | peligrosa porque hace que nos resistamos al aprendizaje real. 19 | 20 | El segundo problema de la calidad es hacer que todos los interesados compartan 21 | lo que entienden por ésta. Este entendimiento compartido nos ayudará a 22 | centrarnos en los objetivos importantes, que son las necesidades del cliente 23 | del proyecto. Esto no significa que sólo sean importantes los aspectos que el 24 | cliente _menciona_: con frecuencia, los clientes dan por sentadas una serie de 25 | propiedades (eficiencia, fiabilidad, etc), y nuestro trabajo es asegurarnos de 26 | que éstas se cumplen tanto como los requisitos explícitos. Esto es, la calidad 27 | necesita de aspectos técnicos como código rápido o diseño simple, pero éstos 28 | deben estar supeditados a las necesidades y los deseos del cliente, implícitos 29 | y explícitos. 30 | 31 | Cómo mejorar la calidad 32 | ----------------------- 33 | Como ya expone el apartado anterior, es imposible dar una «receta mágica» para 34 | mejorar la calidad. De hecho, intentar mejorar la calidad simplemente siguiendo 35 | una lista de pasos es un camino casi seguro hacia el fracaso. No importa qué 36 | pasos sigamos, qué hayamos oído de ellos o quién los haya recomendado: nuestra 37 | única vía para alcanzar un buen nivel de calidad es usar nuestra experiencia y 38 | conocimiento del contexto para decidir qué es lo que nos ayudará en cada 39 | momento. Tenemos que ser conscientes de lo que hacemos y tomar el control de 40 | nuestras decisiones. 41 | 42 | Sin embargo, sí se pueden dar _guías_ para mejorar la calidad. Por ejemplo: 43 | mantener siempre el qué, no el cómo, como la guía de todo lo que hacemos; 44 | mantener el escepticismo y cuestionar cómo hacemos las cosas y por qué; estar 45 | preparado para cambiar cómo trabajamos y luchar contra la «programación de 46 | culto al cargo» footnote:[Hacer las cosas de cierta manera simplemente porque lo 47 | hemos hecho o visto antes, sin entender por qué son así o qué utilidad tienen. 48 | Ver http://en.wikipedia.org/wiki/Cargo_cult_programming[Cargo Cult Programming] 49 | en Wikipedia.]; no creer en la tecnología como el centro de lo que hacemos; 50 | darse cuenta de que lo principal no es hacer código bonito o fácil de entender, 51 | sino resolver problemas footnote:[Los buenos profesionales hacen las dos cosas, 52 | pero es más profesional tener más de lo segundo que más de lo primero.]; no 53 | creer que los problemas tengan una solución única o mejor. 54 | 55 | Esto no quiere decir que las herramientas, técnicas y costumbres no sean 56 | útiles. En absoluto. Lo que sí significa es que éstas nos ayudarán según qué 57 | contextos, según qué proyectos, según qué equipos y según qué clientes, pero 58 | nunca en todos los casos. Los otros artículos de este libro describen algunas 59 | de estas técnicas y herramientas, que son muy útiles y todo buen profesional 60 | debe conocer y dominar, pero uno de los mensajes principales de este artículo 61 | es que conocer y adaptarse al contexto es importante, y que aplicar ciegamente 62 | cualquiera de estas técnicas o herramientas es un error. 63 | 64 | Para ilustrar las ideas de este artículo, y como inspiración para ayudar a 65 | pensar fuera de cánones más académicos, los siguientes apartados muestran una 66 | serie de ejemplos de situaciones junto con sugerencias de posibles soluciones. 67 | Obviamente no puede haber solución única o «correcta» en estos ejemplos, entre 68 | otras razones porque ninguna descripción literaria puede dar toda la 69 | información necesaria para tomar una buena decisión. 70 | 71 | Ejemplo 1: procesos 72 | ------------------- 73 | Como regla general, tener procesos y reglas ayuda a los equipos de desarrollo a 74 | trabajar más eficientemente. Por una parte, facilita que nos concentremos en lo 75 | que estamos haciendo y no en el cómo footnote:[Si siempre hacemos ciertas cosas 76 | de la misma manera y ésta funciona razonablemente bien, no tenemos que gastar 77 | tiempo ni energía decidiendo cómo hacerlas.]. Por otra, facilita la 78 | automatización. 79 | 80 | Sin embargo, los procesos y las reglas pueden quedarse obsoletos. Por ejemplo, 81 | digamos que uno o dos miembros del equipo causan problemas con la integración 82 | continua: con frecuencia hacen cambios que hacen fallar a la batería de pruebas 83 | y no tienen disciplina para escribir pruebas. Ante esta situación, decidimos 84 | que cada cambio enviado al repositorio debe contener alguna modificación a 85 | algún fichero de pruebas. La medida funciona más o menos bien, esos miembros 86 | del equipo empiezan a tomarse las pruebas más en serio, y así aumentamos la 87 | productividad del equipo con ella. 88 | 89 | Ahora bien, todas las reglas tienen partes buenas y partes malas, como todo. 90 | Esta medida concreta puede ser molesta cuando simplemente queramos arreglar una 91 | falta de ortografía en un comentario, actualizar la documentación, o 92 | reformatear el código (ya que nuestro cambio no modificaría ningún fichero de 93 | pruebas). Por tanto, si en algún momento esos miembros del equipo se marchan o 94 | llegan al punto de escribir buenas pruebas y no necesitar la anterior medida, 95 | puede que llegue el momento de eliminar esta regla. Mantener y seguir reglas 96 | simplemente porque «siempre han funcionado» es caer en la tentación de seguir 97 | el «culto al cargo». 98 | 99 | Ejemplo 2: automatización de pruebas 100 | ------------------------------------ 101 | Una de las constantes en los artículos de este libro es el uso de pruebas 102 | automáticas. Aunque es desde luego una de las prácticas más recomendadas y 103 | útiles, también es verdad que con frecuencia tenemos que trabajar con código 104 | legado (es decir, sin pruebas). En esos casos, ¿qué hacemos? 105 | 106 | Por ejemplo, digamos que nos unimos a un equipo que no tiene cultura de pruebas 107 | automáticas, y vamos retrasados con la siguiente entrega. Una parte concreta 108 | del código está dando bastantes problemas, y las pruebas manuales (la manera en 109 | la que el equipo prueba el código) no están encontrando suficientes fallos, o 110 | no lo suficientemente pronto. Aunque automatizar las pruebas es probablemente 111 | la mejor idea a largo plazo, no hay ninguna regla exenta de excepciones 112 | footnote:[Aunque uno podría decir que «no hay ninguna regla exenta de 113 | excepciones» también tiene excepciones...]. Puede que el equipo no tenga 114 | suficiente experiencia en pruebas automáticas como para que introducirlas 115 | mejore la situación _antes de la entrega_. Puede que las partes críticas del 116 | proyecto en el que trabajamos no sean fáciles de probar de manera fiable con 117 | pruebas automáticas, y empeñarnos en poner énfasis en éstas a toda costa no 118 | ayude al equipo en el contexto de esta entrega. Puede que la tensión de ir 119 | retrasados haga al equipo ser «optimistas» al escribir pruebas, y las pruebas 120 | den un falso sentido de seguridad. Puede que los que toman las decisiones no 121 | estén convencidos, y que a corto plazo no valga la pena gastar tiempo y energía 122 | en convencerlos. Y puede que hayamos convencido a los que toman las decisiones, 123 | pero _el equipo_ no esté convencido, e intentar forzarlos a escribir pruebas 124 | sólo vaya a provocar un bajón de moral y un conjunto de pruebas automáticas de 125 | muy mala calidad. 126 | 127 | Y si por la razón que sea llegamos a la conclusión de que intentar implantar 128 | pruebas automáticas para la próxima entrega no es una buena idea, ¿qué podemos 129 | hacer? Una de las posibilidades es empezar haciendo más efectivo al equipo sin 130 | cambiar radicalmente su filosofía de trabajo. Por ejemplo, puede que tras 131 | analizar la situación descubramos que esa parte del programa es más difícil de 132 | probar de manera manual porque no da suficiente información. Quizás añadir una 133 | opción «escondida» que haga esa parte menos opaca puede ser suficiente hasta la 134 | fecha clave. O simplemente mejorar la comunicación entre los miembros del 135 | equipo. Porque, entre otras cosas, siempre es positivo respetar la manera de 136 | trabajar de un equipo («que siempre ha funcionado»): no sólo mejora las 137 | relaciones entre sus miembros, sino que ayuda a ganarse su respeto y atención, 138 | que serán necesarios más tarde para poder implementar cambios grandes como 139 | desarrollo dirigido por pruebas o simplemente escribir pruebas automáticas. Y 140 | mientras tanto, podemos ir enseñando poco a poco al equipo a automatizar las 141 | pruebas para ir cambiando a un modelo (posiblemente) más escalable. 142 | 143 | Ejemplo 3: especificaciones 144 | --------------------------- 145 | Cuando se desarrolla software normalmente se tienen especificaciones. Pueden 146 | ser formales (un documento) o informales (el conocimiento compartido del 147 | equipo). El objetivo de las especificaciones es poder concentrarse en la 148 | solución técnica sin tener que estar preguntando continuamente a los clientes 149 | o al resto del equipo sobre el enfoque a la hora de resolver el problema. Pero, 150 | de nuevo, las especificaciones son sólo una herramienta, y cumplir más a 151 | rajatabla con las especificaciones no tiene por qué garantizar una mayor 152 | calidad del proyecto una vez completado. 153 | 154 | Digamos que estamos desarrollando el software que conduce un coche automático. 155 | Uno de los requisitos del coche es que pare en los pasos de peatones cuando 156 | detecte que una persona está cruzando. Sin embargo, mucha gente no cruza por 157 | los pasos de peatones, así que el coche sería mucho más seguro si no dependiera 158 | de éstos, sino que pudiera detectar por sí mismo si tiene algo delante que 159 | podría llegar a atropellar. Es decir, las especificaciones son una herramienta 160 | muy útil, pero nunca son el objetivo final del desarrollo de software. Las 161 | personas que escriben las especificaciones cometen errores, las condiciones del 162 | proyecto cambian, etc. Mantener siempre el escepticismo y una visión más 163 | completa de nuestros objetivos, y no dejarnos llevar por el «no es mi trabajo», 164 | el «no me pagan por esto» o el «yo sólo hago lo que me dicen», es mucho más 165 | importante que cumplir la especificación diligentemente. Dicho de otra manera, 166 | nuestro trabajo no es escribir software que cumple a rajatabla una descripción 167 | textual de un problema. Es hacer software útil y resolver problemas. 168 | 169 | Ejemplo 4: diseño simple 170 | ------------------------ 171 | Una parte importante de la calidad es, por supuesto, tener código mantenible. 172 | El código mantenible normalmente se consigue con código legible y un diseño 173 | simple. Sin embargo, y como muchas otras cosas, estos dos aspectos son sólo una 174 | herramienta para conseguir calidad: un código legible y un diseño simple hacen 175 | que el código contenga, de media, menos errores, y éstos serán más fáciles de 176 | detectar y corregir. 177 | 178 | Ahora, ¿qué pasa si en algún momento las necesidades del proyecto chocan con 179 | nuestro (por ahora) diseño simple? La respuesta es obvia: las necesidades del 180 | cliente son el objetivo número uno, y lo demás se tiene que adaptar a éstas. 181 | Intentar adaptar las necesidades del cliente al diseño de la aplicación es, en 182 | la mayoría de los casos, un error. Si para resolver el nuevo problema hacemos 183 | el diseño menos lineal o más complejo, no estamos «haciendo una chapuza porque 184 | el cliente no tiene las ideas claras» o porque «no sabe cómo funciona la 185 | aplicación»: estamos ayudando a resolver un problema real. Si eso implica hacer 186 | una «chapuza» en el código, eso probablemente significa que tenemos que revisar 187 | el diseño de nuestra aplicación. No porque lo hayamos hecho mal desde el 188 | principio, sino porque hemos descubierto nuevos requisitos, o refinado los que 189 | teníamos. 190 | 191 | Conclusiones 192 | ------------ 193 | Una conclusión a la que podemos llegar es que la calidad es difícil de 194 | conseguir y de medir, y se necesita experiencia y mucho trabajo para obtenerla. 195 | Pero la conclusión más importante es que _es imposible mejorar la calidad de un 196 | proyecto informático aplicando reglas o metodologías_. Da igual cuánta 197 | experiencia o cuánto conocimiento tenga la persona que las haya formulado, 198 | ningún conjunto de reglas o metodologías puede resolver nuestros problemas si 199 | las aplicamos sin entender lo que hacemos y en qué contexto son útiles. 200 | -------------------------------------------------------------------------------- /integracion_continua.asc: -------------------------------------------------------------------------------- 1 | Integración continua 2 | ==================== 3 | _Yeray Darias Camacho_ 4 | 5 | Independientemente de si el equipo de desarrollo sigue una metodología 6 | clásica en cascada o algún tipo de metodología ágil hay un momento decisivo 7 | que determina el éxito del proyecto. Este momento es el despliegue de la 8 | aplicación en los sistemas del cliente, lo que conocemos como sistema de 9 | producción. 10 | 11 | Generalmente este suele ser un momento muy tenso porque es muy raro que todo 12 | funcione a la primera. Si se sigue una metodología en la que predomine el 13 | desarrollo incremental, donde las funcionalidades se van entregando poco a poco, 14 | se minimiza ligeramente el impacto, siempre y cuando al final de cada iteración 15 | se haya desplegado en el sistema de producción real. Pero generalmente sigue 16 | siendo un momento incómodo, se suelen producir errores porque las máquinas 17 | de desarrollo tienen configuraciones diferentes a las máquina de producción, el 18 | rendimiento no es tan bueno porque la base de datos de producción tiene una 19 | cantidad mucho mayor de información, o cualquier otro detalle que no se tuvo en 20 | cuenta durante el desarrollo. 21 | 22 | Para resolver este problema aparece una nueva «filosofía» o práctica 23 | denominada integración continua. Es un modo de desarrollar un poco diferente al 24 | habitual, y que requiere de una serie de buenas prácticas y la aceptación de las 25 | mismas por el equipo de desarrollo. Se ha de convertir en un hábito que se 26 | realice de forma automática, casi sin darnos cuenta. 27 | 28 | La integración continua desde el punto de vista del desarrollador 29 | ----------------------------------------------------------------- 30 | La siguiente descripción de un día de trabajo, en un equipo que realiza 31 | integración continua, ayudará a ilustrar el proceso y a comprender los elementos 32 | necesarios para llevarla a cabo. 33 | 34 | Al principio del día lo normal es seleccionar la siguiente tarea más importante 35 | a realizar. En base a la reunión de planificación footnote:[Reunión de 36 | aproximadamente una hora en la que se decide cuáles serán las tareas a incluir 37 | en la siguiente versión de la aplicación.] y a la reunión diaria footnote:[Breve reunión de 38 | seguimiento, diaria, que realiza todo el equipo, donde expone en qué se trabajó 39 | el día anterior, en qué se trabajará hoy y si existen impedimentos para llevar a 40 | cabo alguna de las tareas en ejecución.] siempre existe una lista de tareas 41 | priorizadas a disposición del equipo de desarrollo, por lo que es muy sencillo 42 | saber qué es lo siguiente que debemos hacer. Seleccionamos la tarea en la que 43 | debemos trabajar y volvemos a nuestra mesa de trabajo. 44 | 45 | El primer paso será actualizar el código fuente del que disponemos con la 46 | versión más nueva que exista en el repositorio central. De igual manera, si 47 | fuese la primera vez que vamos a trabajar en un proyecto, no tenemos más que 48 | descargar una copia limpia del repositorio de código y empezar a hacer nuestro 49 | trabajo. A lo largo del día implementaremos la nueva funcionalidad, que debería 50 | ser bastante pequeña como para terminarla en una jornada de trabajo y debería 51 | incluir una serie de tests que verifiquen que posee el comportamiento deseado. 52 | Se puede leer más sobre los tests en otros capítulos del libro, ahora mismo no 53 | ahondaremos en el tema porque existen libros enteros sobre TDD y tests 54 | unitarios. 55 | 56 | Cuando la funcionalidad esté terminada, antes de subir ningún cambio al 57 | repositorio, actualizaremos el código fuente con los cambios de nuestros 58 | compañeros y nos aseguraremos de que la aplicación sigue construyéndose 59 | correctamente y que los tests del proyecto están en verde, es decir que pasan 60 | todos sin ningún problema. Si por el contrario aparece algún error lo 61 | arreglaremos inmediatamente. Nunca, bajo ningún concepto, se debe subir código 62 | al repositorio sin revisar que los tests pasan correctamente y la aplicación se 63 | puede construir sin incidencias. Además, es recomendable acceder a la aplicación 64 | y revisar rápidamente que todo sigue funcionando adecuadamente, ya que por lo 65 | general los tests unitarios no tienen una cobertura del 100%, puesto que ciertos 66 | detalles de infraestructura son más sencillos de probar a mano. 67 | 68 | Una vez hemos realizado todos los pasos anteriores podemos guardar nuestros cambios 69 | en el repositorio de código central, lo que permite que el resto del equipo se 70 | actualice y los tengan disponibles en cuestión de segundos. 71 | 72 | Aunque el proceso de integración continua comenzó desde el momento en el que 73 | empezamos a trabajar en la nueva funcionalidad, el servidor de integración 74 | continua comienza a trabajar cuando subimos nuestros cambios al repositorio de 75 | código. Dicho sistema se descargará una versión nueva del código fuente con todos los 76 | cambios realizados (los nuestros y los de nuestros compañeros), ejecutará los tests y 77 | tras hacer la construcción del proyecto lo desplegará en una «réplica» de la máquina 78 | de producción. Todo de forma totalmente automatizada. El propio servidor de 79 | integración continua podría pasar algunas pruebas extras, como por ejemplo 80 | análisis estático de código, análisis de cobertura, o cualquier otro detalle que 81 | sería muy tedioso pasar en el proceso de desarrollo porque requiere demasiado 82 | tiempo. 83 | 84 | También podría haber ocurrido un error. En ese caso el servidor de integración 85 | continua nos avisaría con algún mensaje en la consola del mismo o simplemente 86 | con el envío de un correo electrónico, que es la opción más común. En ese 87 | caso tendremos que inspeccionar cuál ha sido el error y resolverlo para subir 88 | una nueva versión corregida. Como el error se genera a los pocos minutos, y no 89 | han pasado días ni meses, es muy fácil encontrar dónde está el problema. 90 | 91 | Ventajas de la integración continua 92 | ----------------------------------- 93 | La experiencia demuestra que reduce de manera increíble el número de errores en 94 | el producto final. Esta es probablemente la mayor ventaja que aporta, porque 95 | un producto final con pocos o incluso ningún error es a fin de cuentas el 96 | objetivo que todos deseamos como desarrolladores. 97 | 98 | Pero no hay que olvidarse de la otra gran ventaja que aporta esta práctica, la 99 | transparencia del proceso. Como todos trabajamos utilizando el mismo repositorio 100 | de código y la información del servidor de integración es pública, para todo el 101 | equipo e incluso a veces para el cliente, se conoce con precisión el estado real 102 | del proyecto. No hay que esperar durante meses para saber cómo van las cosas y 103 | las reuniones de seguimiento pueden ser cortas y precisas. 104 | 105 | Como ventaja añadida cabe destacar la posibilidad de disponer de un servidor de 106 | demostración en el que siempre se posee la última versión de la aplicación. De 107 | esta manera los clientes pueden probar los últimos cambios día a día y ofrecer 108 | información al equipo de desarrollo sin tener que esperar meses. 109 | 110 | Requisitos de la integración continua 111 | ------------------------------------- 112 | Ahora ya disponemos de una visión más concreta de lo que es la integración 113 | continua, pero nos vendrá bien repasar cuáles son los elementos indispensables 114 | para hacerlo correctamente. 115 | 116 | Repositorio de código 117 | ~~~~~~~~~~~~~~~~~~~~~ 118 | 119 | El repositorio de código es fundamental la herramienta que permite que el equipo 120 | trabaje de forma totalmente sincronizada, pero a la vez sencilla. Cada uno trabaja 121 | en su parte y puede actualizar los cambios sin necesidad de que otros 122 | desarrolladores tengan que esperar por estos y viceversa. 123 | 124 | Desgraciadamente todavía hay muchas empresas que aún no poseen una herramienta 125 | de este tipo. Para aquellos que aún no utilicen un repositorio de código cabe 126 | destacar que actualmente existen una gran cantidad de soluciones gratuitas de 127 | calidad, como pueden ser Subversion, Git o Mercurial por mencionar algunas. Esta 128 | es una pieza fundamental y será prácticamente imposible realizar integración 129 | continua si no se dispone de ella. 130 | 131 | Este tipo de herramientas marcan un antes y un después en el trabajo cotidiano 132 | de un equipo de desarrollo de software. Pero además es el pivote fundamental de 133 | la integración continua, que no es posible sin un repositorio de código. 134 | 135 | Sistema de construcción automatizado 136 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 137 | 138 | Muchos equipos utilizan complejos entornos de desarrollo para implementar y compilar 139 | los proyectos. Esto en sí mismo no es malo, pero debemos poder construir el proyecto 140 | en cualquier máquina sin necesidad de dichos entornos. No todas las máquinas en las 141 | que vayamos a instalar nuestro proyecto tendrán el entorno de desarrollo con el que 142 | trabajamos, incluso en algunos casos estos pueden tener licencias relativamente 143 | caras, por lo que no sería una opción viable. 144 | 145 | Hay muchas soluciones de construcción diferentes en función de los lenguajes 146 | de programación, en Java están Ant o Maven, en Ruby está Rake, solo por 147 | mencionar algunas. La condición es que debe ser una herramienta muy sencilla que 148 | se pueda ejecutar en cualquier máquina y que consiga automatizar no solo el 149 | proceso de compilación, sino la ejecución de los tests de nuestra aplicación e 150 | incluso el despliegue en el servidor de aplicaciones llegado el caso. 151 | 152 | Commits diarios 153 | ~~~~~~~~~~~~~~~ 154 | 155 | No todos los requisitos se basan en tener un determinado tipo de herramienta, 156 | algunos se basan en saber cómo utilizar una determinada herramienta. Una vez que 157 | tenemos un control de versiones hay que recordar que el almacenamiento de los 158 | cambios debe ser diario, idealmente un desarrollador debe hacer incluso varias 159 | subidas al repositorio de código al día. 160 | 161 | Si tenemos un repositorio de código, pero cada desarrollador sube sus cambios 162 | cada dos semanas seguimos en el mismo problema de antes, la integración del 163 | proyecto se retrasa durante todo ese tiempo, perdiendo toda opción de obtener 164 | información inmediatamente. Además, retrasar la integración tanto tiempo genera más 165 | conflictos entre los ficheros editados por los desarrolladores. Debido a que con cada 166 | actualización del repositorio se hace una integración del proyecto completo, cada 167 | mínimo cambio que no desestabilice el proyecto debe ser subido para que se compruebe 168 | que todo sigue funcionando correctamente. 169 | 170 | Pruebas unitarias 171 | ~~~~~~~~~~~~~~~~~ 172 | 173 | Hemos hablado todo el tiempo de comprobar que el proyecto funciona correctamente 174 | y al mismo tiempo de automatizar todo el proceso lo mejor posible. Para poder 175 | comprobar que el proyecto funciona de forma automática necesitamos pruebas 176 | unitarias (muchas veces incluso pruebas de integración). 177 | 178 | Existen muchas maneras diferentes de incluir tests en el proyecto, personalmente 179 | creo que TDD es la manera más adecuada, pero si el equipo no tiene experiencia y 180 | crea las pruebas después de haber realizado la implementación tampoco es un 181 | método inválido. La premisa fundamental es que toda nueva funcionalidad debe 182 | tener una batería de pruebas que verifique que su comportamiento es correcto. 183 | 184 | Servidor de integración 185 | ~~~~~~~~~~~~~~~~~~~~~~~ 186 | 187 | Esta es la pieza más polémica, mucha gente cree que no es absolutamente 188 | necesaria para hacer integración continua correctamente, por ejemplo, algunos 189 | equipos hacen la integración de forma manual con cada subida al repositorio de código. 190 | En mi opinión es un paso tan sencillo y barato de automatizar que no merece la pena 191 | ahorrárselo. Montar algún servidor de integración, como por ejemplo Jenkins o Cruise 192 | Control, es gratuito y tan sencillo como desplegar un fichero en un servidor. Por 193 | contra las ventajas son grandísimas. Para empezar el proceso está totalmente 194 | automatizado, lo que evita el error humano. Y por otro lado reduce la cantidad 195 | de trabajo, ya que tenemos una solución prefabricada sin necesidad de tener que 196 | crear nosotros mismos complejas soluciones caseras. 197 | 198 | Un paso más allá 199 | ---------------- 200 | Con los pasos descritos hasta el momento ya tendríamos un proceso bastante 201 | completo y que a buen seguro mejorará enormemente la calidad de nuestro 202 | producto. Pero podemos ir un poco más allá y utilizar el servidor de integración 203 | para que haga ciertas tareas relativamente complejas por nosotros. 204 | 205 | Por ejemplo, podríamos configurar el servidor para que haga análisis estático de 206 | código, de forma que pueda buscar en todo el código bloques sospechosos, 207 | bloques duplicados o referencias que no se utilizan, entre otras cosas. Existen 208 | gran cantidad de opciones como pueden ser FindBugs o PDM. A simple vista puede 209 | parecer algo irrelevante, pero hay que recordar que la complejidad es lo que más 210 | ralentiza el proceso de desarrollo, por lo que un código con menor número de 211 | líneas y referencias inútiles será más sencillo de leer y entender. 212 | 213 | También podríamos incluir tareas que permitan desplegar la aplicación en un 214 | servidor, de forma que tendríamos siempre un servidor de demostración con la 215 | última versión de nuestro proyecto. Esto es realmente útil cuando tenemos un 216 | cliente comprometido, dispuesto a probar todos los nuevos cambios y a dar información 217 | al equipo. 218 | 219 | Otra operación que podemos automatizar, y que el servidor de integración podría 220 | hacer por nosotros, es la realización de pruebas de sistema sobre la aplicación. 221 | Imaginemos que estamos desarrollando una aplicación web, podríamos crear tests 222 | con alguna herramienta de grabación de la navegación, como por ejemplo Selenium, 223 | y lanzarlos con cada construcción que haga el servidor. Es un tipo de prueba que 224 | requiere mucho tiempo y no sería viable que se lancen con cada compilación del 225 | desarrollador, pero para el servidor de integración no habría ningún problema. 226 | Este es solo un ejemplo más de la cantidad de cosas que puede hacer un servidor 227 | de integración continua por nosotros, y que nos ayudará a mantener un producto 228 | estable y testeado de manera totalmente automática. 229 | 230 | Para acabar me gustaría utilizar algunos comentarios escuchados por Martin 231 | Fowler cuando habla de integración continua con otros desarrolladores. La 232 | primera reacción suele ser algo como «eso no puede funcionar (aquí)» o «hacer 233 | eso no cambiará mucho las cosas», pero muchos equipos se dan cuenta de que es 234 | más fácil de implementar de lo que parece y pasado un tiempo su reacción 235 | cambia a «¿cómo puedes vivir sin eso?». Ahora es tu elección si decides 236 | probarlo o no, pero antes de hacerlo piensa en lo poco que tienes que perder y 237 | lo mucho que puedes ganar. 238 | -------------------------------------------------------------------------------- /documentacion_activa.asc: -------------------------------------------------------------------------------- 1 | Documentación activa 2 | ==================== 3 | _Joaquín Caraballo_ 4 | 5 | Si en un extremo están los que definen el problema a resolver y en el otro los que lo resuelven, ¿cómo podemos asegurarnos de que todos estamos intentando resolver el mismo problema? Y, si aceptamos que la definición va a necesitar ser enriquecida, corregida, matizada, e incluso que el problema a resolver va a cambiar radicalmente durante la vida del proyecto, ¿cómo mantenemos una misma definición para todos los interesados, y más importante, cómo aseguramos que nuestro programa resuelve el problema? 6 | 7 | 8 | Las pruebas funcionales 9 | ----------------------- 10 | 11 | Para tener una cierta confianza en que la aplicación resuelve un cierto problema se llevan a cabo pruebas funcionales; estas resuelven un ejemplo concreto y representativo simulando las acciones del usuario y verificando que la reacción de la aplicación es la esperada. 12 | 13 | Las pruebas funcionales han de mantenerse al día y evolucionar con los cambios de la aplicación y sus requisitos. Otro de los retos es que sean correctas, es decir, que el comportamiento que estén verificando sea realmente el que se requiere de la aplicación. 14 | 15 | Es crucial que las pruebas funcionales estén automatizadas; lo típico es escribirlas en el mismo lenguaje de programación de la aplicación. Esto nos permite ejecutar un gran número de ellas de forma sistemática y tras cada cambio, con lo que también nos protegerán de que algo que funcionaba deje de hacerlo, es decir, de posibles _regresiones_. 16 | 17 | 18 | neverread 19 | ~~~~~~~~~ 20 | 21 | Supongamos que tenemos un cliente con falta de tiempo para leer artículos de Internet. 22 | 23 | ____________________________________________________________________ 24 | -Tantos artículos interesantes, no tengo tiempo para leerlos, pero tampoco puedo ignorarlos. Me gustaría tener una aplicación en la que copiar la dirección del artículo que me interese. 25 | 26 | -¡Ah!, ¿quieres una aplicación que te permita almacenar enlaces a artículos? La aplicación mantendría una lista con los artículos almacenados, a la que luego podrías acceder cuando tengas tiempo y leer los artículos... 27 | 28 | -No, no, si yo lo que quiero es que los enlaces a artículos desaparezcan, si tuviera tiempo para leerlos después usaría instapaper, hombre. Estaría bien que tuviese una lista de artículos por leer siempre vacía, me daría una sensación genial de tenerlo todo bajo control. 29 | 30 | -Er... vale. 31 | ____________________________________________________________________ 32 | 33 | De la conversación hemos conseguido extraer un criterio de aceptación: 34 | ____ 35 | Por mucho que añadamos artículos, estos no se añadirán a una lista de artículos por leer. 36 | ____ 37 | 38 | La prueba funcional correspondiente, verificará que la aplicación cumple el criterio para un ejemplo concreto: 39 | ____ 40 | Cuando añadimos el artículo `art.culo/interesante.html`, la lista de artículos por leer permanece vacía. 41 | ____ 42 | 43 | Las pruebas funcionales, si bien no tienen necesariamente que serlo, se llevan a cabo con frecuencia como pruebas de punta a punta _(end to end)_ (<<goos>> pág 10), es decir, pruebas en las que se ejercita la aplicación en su conjunto y desde afuera, simulando las acciones del usuario y de los sistemas colaboradores, y evaluando la corrección según la perciben usuario y colaboradores. 44 | 45 | Hemos decidido resolver el problema propuesto con una aplicación web, _neverread_. Implementaremos en Java una prueba funcional de punta a punta que verifique el criterio de aceptación con una prueba _junit_ que va a iniciar la aplicación y a continuación ejercitarla y evaluarla. Para simular la interacción de un usuario a través de un navegador web utilizaremos Selenium/WebDriver/HtmlUnit: 46 | 47 | .Prueba punta a punta en Java (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/active-documentation/src/test-acceptance/purejava/ListStaysEmptyTest.java[purejava/ListStaysEmptyTest.java]) 48 | [source,java] 49 | ----------------------------------------------------------------------------- 50 | public class ListStaysEmptyTest { 51 | private WebDriver webDriver; 52 | private NeverReadServer neverread; 53 | 54 | @Test 55 | public void articleListStaysEmptyWhenAddingNewArticle() { 56 | webDriver.findElement(By.name("url")).sendKeys("interesting/article.html", Keys.ENTER); 57 | assertThat(webDriver.findElements(By.cssSelector("li")).size(), is(0)); 58 | } 59 | 60 | @Before 61 | public void setUp() throws Exception { 62 | neverread = new NeverReadServer(); 63 | neverread.start(8081); 64 | 65 | WebDriver driver = new HtmlUnitDriver(); 66 | driver.get("http://localhost:8081"); 67 | webDriver = driver; 68 | } 69 | 70 | @After 71 | public void tearDown() throws Exception { 72 | webDriver.close(); 73 | neverread.stop(); 74 | } 75 | } 76 | ----------------------------------------------------------------------------- 77 | 78 | Documentación activa 79 | ------------------- 80 | 81 | Si bien podríamos considerar que nuestra prueba funcional en Java es la documentación principal donde se registran los criterios de aceptación; dependiendo de quién vaya a consumirla, esto puede o no ser una opción. Además, conseguir reemplazar la legibilidad de un párrafo en español con una prueba en el lenguaje de programación es todo un reto, particularmente si el lenguaje de programación es tan prolijo como Java. 82 | 83 | En consecuencia, en la mayoría de los proyectos es necesario documentar los requisitos de manera más textual. 84 | 85 | Una forma de intentar obtener lo mejor de las dos alternativas es conectar la documentación a la aplicación de tal forma que en todo momento sea automático y evidente verificar el cumplimiento de cada uno de los requisitos expresados, lo que nos ayudará a mantener la documentación sincronizada con la aplicación y con el cliente. 86 | 87 | La documentación, junto con la capacidad de procesarla para su verificación, servirá así de batería de pruebas funcionales; si ejecutamos esta batería con frecuencia (idealmente con cada cambio) y la mantenemos veraz, estaremos garantizando el correcto funcionamiento de la aplicación... para la definición especificada. 88 | 89 | 90 | Concordion 91 | ---------- 92 | 93 | Concordion es una de las herramientas que nos pueden ayudar a conectar la documentación con la aplicación; en Concordion los criterios se escriben en HTML al que se añaden algunas marcas que comienzan con `concordion:` 94 | 95 | .Primera prueba usando Concordion (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/active-documentation/src/test-acceptance/concordion/v1/ListStaysEmpty.html[concordion/v1/ListStaysEmpty.html]) 96 | [source,html] 97 | ----------------------------------------------------------------------------- 98 | <p> 99 | When an article is added, the list of unread articles stays empty 100 | </p> 101 | 102 | <div class="example"> 103 | <h3>Example</h3> 104 | 105 | <p> 106 | When the article 107 | <b concordion:set="#url">interesting/article.html</b> 108 | is added, the list of unread articles stays 109 | <b concordion:assertEquals="articleListAfterAdding(#url)">empty</b> 110 | </p> 111 | </div> 112 | ----------------------------------------------------------------------------- 113 | 114 | Vemos que hemos expresado el ejemplo en forma de condición y consecuencia que se debe verificar. Los atributos `concordion:set` y `concordion:assertEquals` conectan el documento al método de la clase de respaldo, escrita en Java, `String articleListAfterAdding(String url)`, que se encargará de hacer lo que dice el texto. 115 | 116 | .Clase de respaldo de la prueba en Concordion (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/active-documentation/src/test-acceptance/concordion/v1/ListStaysEmptyTest.java[purejava/ListStaysEmptyTest.java]) 117 | [source,java] 118 | ----------------------------------------------------------------------------- 119 | public class ListStaysEmptyTest extends ConcordionTestCase { 120 | private WebDriver webDriver; 121 | private NeverReadServer neverread; 122 | 123 | @SuppressWarnings(value = "unused") 124 | public String articleListAfterAdding(String url) throws InterruptedException { 125 | webDriver.findElement(By.name("url")).sendKeys(url, Keys.ENTER); 126 | List<WebElement> pendingArticles = webDriver.findElements(By.cssSelector("li")); 127 | 128 | return convertListOfArticlesToString(pendingArticles); 129 | } 130 | 131 | private static String convertListOfArticlesToString(List<WebElement> pendingArticles) { 132 | if (pendingArticles.isEmpty()) return "empty"; 133 | else { 134 | StringBuilder stringBuilder = new StringBuilder(); 135 | stringBuilder.append(pendingArticles.get(0).getText()); 136 | 137 | for (int i = 1; i < pendingArticles.size(); i++) 138 | stringBuilder.append(", ").append(pendingArticles.get(i).getText()); 139 | 140 | return stringBuilder.toString(); 141 | } 142 | } 143 | ----------------------------------------------------------------------------- 144 | 145 | Al ejecutar `ListaPermaneceVaciaTest`, Concordion generará un documento HTML con el texto anterior en el que se indicará si se cumple la aserción resaltándola en verde o rojo. 146 | 147 | 148 | Paso a paso 149 | ~~~~~~~~~~~ 150 | 151 | Veamos qué es lo que está pasando aquí. Hemos escrito el criterio de aceptación en `ListaPermaneceVacia.html`. Acompañando al HTML hemos escrito una clase en Java que extiende una clase de infrastructura de Concordion: `class ListaPermaneceVaciaTest extends ConcordionTestCase`. 152 | 153 | Cuando ejecutamos `ListaPermaneceVaciaTest`: 154 | 155 | . Concordion procesa el HTML. 156 | . Concordion detecta la marca `concordion:set="#url"` y guarda el contenido de esa marca HTML (en este caso, «art.culo/interesante.html») en la variable de Concordion `#url`. 157 | . Concordion detecta la marca `concordion:assertEquals="articleListAfterAdding(#url)"`, por lo que busca en la clase de acompañamiento un método denominado `articleListAfterAdding` y lo ejecuta, pasándole el contenido de `#url` como parámetro. 158 | . El método `articleListAfterAdding` simula la acción de un usuario que introduce `url` y obtiene la lista de artículos resultante. 159 | . Mediante `convertListOfArticlesToString`, transformamos la lista producida por WebDriver en una representación textual que pueda ser comparada con el texto del HTML. Hemos decidido que la representación textual de una lista vacía sea «vacía». 160 | . El método `articleListAfterAdding` retorna, devolviendo una cadena (en este caso «vacía») que es comparada con el contenido de la marca HTML en el que se encontró `concordion:assertEquals`. 161 | . Concordion termina de procesar el documento HTML y genera otro HTML en el que el texto que tiene la marca `concordion:assertEquals` está resaltado en verde, para indicar que la aserción se cumple. 162 | 163 | 164 | Manteniendo el nivel de abstracción apropiado 165 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 166 | 167 | Es importante esforzarse en describir el funcionamiento de la aplicación en términos del dominio. Por ejemplo, podríamos haber caído en la tentación de escribir el ejemplo como _Cuando el usuario entra una cadena en la caja de texto y pulsa enter, la lista de artículos está vacía_. Sin embargo, eso sería perjudicial porque nos alejaría de la persona que define lo que debe hacer la aplicación y resultaría más _frágil_, es decir, en cuanto decidiéramos cambiar la implementación, por ejemplo, supongamos que las direcciones se introducen arrastrándolas a una zona de la aplicación, tendríamos que reescribir el documento. 168 | 169 | A poco que la documentación activa crezca, las clases de respaldo van a necesitar una cantidad importante de código. Algunas abstracciones pueden ayudarnos reducir la repetición y la fragilidad de las clases de respaldo. 170 | 171 | Podemos hacer que la clase de respaldo sólo _hable_ en el lenguaje del dominio, para lo cual hemos de desarrollar un lenguaje dedicado, con lo que el método quedaría algo así: 172 | 173 | .Método de respaldo usando lenguaje del dominio (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/active-documentation/src/test-acceptance/concordion/v2_appdriver/ListStaysEmptyTest.java[concordion/v2_appdriver/ListStaysEmptyTest.java]) 174 | [source,java] 175 | ----------------------------------------------------------------------------- 176 | public String articleListAfterAdding(String article) throws InterruptedException { 177 | driver.addArticle(article); 178 | return convertListOfArticlesToString(driver.getListOfArticles()); 179 | } 180 | ----------------------------------------------------------------------------- 181 | 182 | Otra posibilidad es abstraer la página web en términos de los elementos del entorno gráfico, es decir, que hable de elementos visuales de la página. 183 | 184 | .Método de respaldo usando lenguaje del entorno gráfico (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/active-documentation/src/test-acceptance/concordion/v3_pagedriver/ListStaysEmptyTest.java[concordion/v3_pagedriver/ListaStaysEmptyTest.java]) 185 | [source,java] 186 | ----------------------------------------------------------------------------- 187 | public String articleListAfterAdding(String url) throws InterruptedException { 188 | page.enterIntoNewArticlesTextBox(url); 189 | List<String> pendingArticles = page.getArticlesInListOfArticles(); 190 | 191 | return convertListOfArticlesToString(pendingArticles); 192 | } 193 | ----------------------------------------------------------------------------- 194 | 195 | La capa de abstracción en términos de lenguaje del dominio es la opción más _pura_, pero dependiendo del proyecto podremos preferir una capa que se exprese en términos gráficos o ambas, dependiendo de la complejidad del proyecto y de cuán involucrado esté el cliente en los detalles gráficos. 196 | 197 | 198 | Pruebas asíncronas 199 | ------------------ 200 | 201 | En las secciones anteriores nos hemos permitido hacer un poco _trampas_ que deberíamos descubrir antes de cerrar el artículo. Supongamos que el desarrollador, al escribir el código de la aplicación comete una equivocación por no entender debidamente lo que necesita el cliente; decide hacer una aplicación que _añade_ los artículos a una lista de artículos a leer. Nuestras pruebas funcionales deberían detectar este error marcando la aserción en rojo. Sin embargo, nos encontramos con que la pruebas pasan. 202 | 203 | Evidentemente, nuestras pruebas funcionales no son correctas, esto se debe a que estamos verificando que el estado de la lista de artículos es el mismo después de entrar el nuevo artículo que antes de entrarlo, y la prueba verifica la condición antes de que la aplicación tenga tiempo de añadir erróneamente artículos a la lista. 204 | 205 | Probar sistemas asíncronos es lo suficientemente complejo como para justificar un artículo en sí mismo, pero si enumeramos algunas de las opciones, de más rápidas y sencillas de implementar a menos, tenemos: 206 | 207 | . Probamos sólo la parte síncrona del sistema. Esto hace las pruebas más sencillas y rápidas a costa de reducir el alcance. 208 | . Introducimos puntos de sincronización. Volveremos a este en un segundo. 209 | . Verificamos periódicamente la aserción hasta que se cumpla o se agote el tiempo de espera. En esta opción es crucial ajustar la duración, si esperamos demasiado las pruebas tardarán demasiado innecesariamente, si esperamos demasiado poco tendremos falsos negativos. 210 | 211 | En nuestro ejemplo sabemos que, cada vez que la aplicación responde a la entrada de un nuevo artículo, lo último que hace es borrar la caja de texto. Por lo tanto, podemos utilizar este evento como punto de sincronización, es decir, antes de verificar que la lista permanece vacía esperaremos a que la caja se haya borrado. 212 | 213 | 214 | .Ejemplo de punto de sincronización (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/active-documentation/src/test-acceptance/concordion/v5_with_synchronisation/tools/NeverReadDriver.java[concordion/v5_with_synchronisation/tools/NeverReadDriver.java]) 215 | [source,java] 216 | ----------------------------------------------------------------------------- 217 | public void addArticle(String url) { 218 | webDriver.findElement(By.name("url")).sendKeys(url, Keys.ENTER); 219 | 220 | new WebDriverWait(webDriver, 2).until(new ExpectedCondition<Object>() { 221 | @Override 222 | public Object apply(WebDriver webDriver) { 223 | return "".equals(webDriver.findElement(By.name("url")).getAttribute("value")); 224 | } 225 | }); 226 | } 227 | 228 | public List<String> getListOfArticles() { 229 | return webElementsToTheirTexts(webDriver.findElements(By.cssSelector("li"))); 230 | } 231 | ----------------------------------------------------------------------------- 232 | 233 | 234 | Conclusión 235 | ---------- 236 | 237 | La _documentación activa_ es una forma de probar funcionalmente un programa en el que cada criterio de aceptación se enuncia con un texto que se enlaza a la ejecución de código que verifica el criterio. Al ejecutarla, se produce un resultado que indica, de forma legible para los expertos del dominio, qué criterios de aceptación cumple el programa y qué criterios no cumple. Como casi todo, su uso debería adaptarse a la composición del equipo y la complejidad del proyecto. 238 | -------------------------------------------------------------------------------- /funcionales.asc: -------------------------------------------------------------------------------- 1 | Lecciones de aprender un lenguaje funcional 2 | =========================================== 3 | _Esteban Manchado Velázquez_ 4 | 5 | Los lenguajes funcionales son una familia de lenguajes que la mayoría de los 6 | programadores conoce de oídas, pero desgraciadamente no muchos conocen 7 | suficientemente bien. Y digo «desgraciadamente» porque, independientemente de 8 | que por una razón u otra los lenguajes funcionales son muy poco demandados en 9 | el mercado laboral, aprenderlos nos puede brindar muchas ideas interesantes, 10 | patrones, buenas costumbres y lecciones que podemos aplicar a muchos otros 11 | lenguajes. 12 | 13 | Es difícil trazar una línea clara entre los lenguajes funcionales y los 14 | lenguajes no funcionales, pero podemos citar Lisp, Haskell, Erlang, Scala y 15 | Clojure como los lenguajes funcionales más populares actualmente. Muchos 16 | lenguajes de programación populares tienen algunos rasgos funcionales, como 17 | Javascript, Ruby y Python. Los lenguajes funcionales, o en general, de 18 | cualquier paradigma al que no estemos acostumbrados, nos pueden dar ideas que 19 | podemos aplicar no solamente en estos lenguajes que tienen rasgos funcionales, 20 | sino en casi cualquier lenguaje. 21 | 22 | Aprender un lenguaje funcional lo suficiente como para tener unas nociones e 23 | inspirarse, no tiene por qué llevar mucho tiempo. Además, no sólo disponemos de 24 | Internet, sino también de excelentes libros que están pensados precisamente 25 | para programadores que vienen de otros lenguajes. El resto de este artículo 26 | explora algunas técnicas, buenas costumbres e ideas comunes en lenguajes 27 | funcionales, que podemos aplicar fácilmente en otros lenguajes. Por supuesto, 28 | algunas de estas lecciones se pueden aprender simplemente por experiencia, y no 29 | son necesariamente exclusivas de los lenguajes funcionales. 30 | 31 | Los lenguajes son diferentes 32 | ---------------------------- 33 | Los lenguajes de programación son como los idiomas humanos en muchos sentidos: 34 | tienen sintaxis, expresiones comunes, son mejores o peores que otros para 35 | expresar ciertas ideas, se pueden «hablar» con mayor o menor fluidez e incluso 36 | se puede decir que sus «hablantes» tienen una cierta «cultura» diferente de los 37 | «hablantes» de otros lenguajes. 38 | 39 | Así, cuando aprendemos un lenguaje nuevo es un error verlo como «una nueva 40 | sintaxis»: aprender un lenguaje bien, nos hace cambiar cómo pensamos y cómo 41 | resolvemos los problemas. Por ejemplo, digamos que empezamos a aprender Lisp y 42 | que sólo conocemos lenguajes imperativos «clásicos» como C o Java. Como parte 43 | de un programa que estamos escribiendo, tenemos una función que calcula el 44 | valor total a pagar por unos artículos. Los parámetros de entrada son el 45 | importe de cada artículo, el número de artículos, el porcentaje de impuesto, y 46 | el límite pasado el cual hay que aplicar el impuesto (p.ej.: tenemos 2 47 | artículos a 5 euros cada uno, un impuesto del 10% y un límite de 8 euros; el 48 | precio final será 11. Si el límite fuese 15 euros, el precio final sería 10 49 | porque no se aplicaría el impuesto). Una forma de escribirla en Lisp sería la 50 | siguiente: 51 | 52 | [source,lisp] 53 | ._Mal_ ejemplo de cómo resolverlo en Lisp (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/functional-languages/calculate-prices-bad.lisp[calculate-prices-bad.lisp]) 54 | (defun calculate-price-BAD (cost nitems limit percent-tax) 55 | (setq total-price (* cost nitems)) 56 | (if (> total-price limit) 57 | (progn 58 | (setq tax (* total-price (/ percent-tax 100))) 59 | (setq total-price (+ total-price tax)))) 60 | total-price) 61 | 62 | Sin embargo, si escribimos la función así no aprendemos nada nuevo y mostramos 63 | nuestro «acento extranjero» al escribir Lisp. Este código no es legible ni para 64 | programadores de Java ni para programadores de Lisp, y además no aprovechamos 65 | las ventajas del lenguaje, mientras que sufrimos sus desventajas. 66 | Simplemente usamos Lisp como «un mal Java». Compárese el anterior código con el 67 | siguiente: 68 | 69 | [source,lisp] 70 | .Solución usando Lisp de una forma más convencional (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/functional-languages/calculate-prices.lisp[calculate-prices.lisp]) 71 | (defun calculate-price (cost nitems limit percent-tax) 72 | (let* ((total-price (* cost nitems)) 73 | (tax (* total-price (/ percent-tax 100)))) 74 | (if (> total-price limit) 75 | (+ total-price tax) 76 | total-price))) 77 | 78 | Este código se parece más a lo que muchos programadores de Lisp esperarían o 79 | escribirían ellos mismos, y a la mayoría de las personas que sepan un poco de 80 | Lisp les parecerá más legible y fácil de mantener. La razón es que estamos 81 | jugando con las fortalezas de Lisp, en vez de intentar adaptar el lenguaje a 82 | nuestros conocimientos previos. 83 | 84 | Concretamente, el segundo ejemplo se aprovecha de dos detalles muy comunes en 85 | Lisp que no son tan comunes en lenguajes no funcionales: 86 | 87 | 1. No usar variables, sino «valores con nombre». O, dicho de otra manera, 88 | evitar asignar más de una vez a cada variable. Esto se puede ver en el bloque 89 | +let*+: las dos «variables» del bloque, +total-price+ y +tax+, nunca 90 | reciben otros valores. Así, el bloque +let*+ contiene una lista de valores con 91 | nombres simbólicos, lo que hace el código más claro y mantenible. 92 | 2. La construcción +if+ sigue el patrón funcional de ser una _expresión_ que 93 | devuelve un valor. En este caso, recibe una condición y dos expresiones: la 94 | primera se devuelve si la condición es verdadera, y la segunda si la condición 95 | resulta ser falsa. Funciona de una forma parecida al operador ternario de C y 96 | Java (+valor = condicion ? valorsiverdadero : valorsifalso+). 97 | 98 | Sabiendo esto, y que la última expresión de una función Lisp es el valor 99 | devuelto por ésta, se puede entender el segundo ejemplo mucho mejor. 100 | 101 | 102 | Pensar en el objetivo, no en el proceso 103 | --------------------------------------- 104 | Aunque el ejemplo de arriba es bastante simplista, también ilustra que los 105 | lenguajes funcionales tienden a hacerte pensar más en el objetivo que en el 106 | proceso. Para algoritmos, generalmente una solución en un lenguaje funcional se 107 | parece más a la definición matemática. En el primer ejemplo, la implementación 108 | de la función está basada en el proceso de calcular. En el segundo, en el 109 | significado de las operaciones. 110 | 111 | Si lo pensamos en castellano, el primer ejemplo sería algo así: 112 | 113 | [quote] 114 | Primero multiplicamos +cost+ por +nitems+ para calcular el precio base, y lo 115 | guardamos en +total-price+. Si éste está por encima de +limit+, entonces 116 | primero calculamos +tax+ a base de multiplicar +total-price+ por +percent-tax+ 117 | dividido por 100, y luego guardamos en +total-price+ la suma del antiguo 118 | +total-price+ más +tax+. El resultado es el valor guardado en +total-price+ al 119 | final del proceso. 120 | 121 | El segundo ejemplo, en cambio, sería algo así: 122 | 123 | [quote] 124 | El resultado es la suma de +total-price+ y +tax+ si +total-price+ está por 125 | encima de +limit+, o +total-price+ en caso contrario. Definimos 126 | +total-price+ como la multiplicación de +cost+ por +nitems+, y +tax+ 127 | como la multiplicación de +total-price+ por +percent-tax+ dividido por 100. 128 | 129 | Nótese cómo en la segunda explicación podemos dejar para el final la 130 | explicación de los valores declarados en el bloque +let*+. En muchos casos, y 131 | siempre que hayamos escogido buenos nombres para esos valores, no hará falta 132 | leer el bloque +let*+ para entender la función. 133 | 134 | Usar variables que no cambian una vez les hemos asignado un valor, es una 135 | buena costumbre porque hace más fácil entender de dónde sale cada valor. Por 136 | esa razón, el lenguaje Scala distingue entre dos tipos de variables: +var+ y 137 | +val+. El segundo tipo, que es el más usado con diferencia, declara una 138 | variable _inmutable_, por lo que el compilador nos asegura que no podemos 139 | asignar ningún otro valor una vez declarada. 140 | 141 | Esto está relacionado con el siguiente apartado, «Diseño de abajo a arriba, 142 | funciones pequeñas»: para poder pensar en el significado, muchas de las 143 | operaciones tienen que abstraerse en pequeñas funciones. 144 | 145 | 146 | Diseño de abajo a arriba, funciones pequeñas 147 | -------------------------------------------- 148 | Otra característica común de la programación en Lisp es intentar acercar el 149 | lenguaje a la tarea que se intenta resolver. Una de las maneras de hacerlo es 150 | escribir funciones que, aunque sean pequeñas y simples, escondan detalles de 151 | implementación que no nos interesen y que suban el nivel de abstracción. La 152 | lección aquí es que escribir funciones y métodos de una o dos líneas es útil 153 | _si_ suben el nivel de abstracción. No se trata de «esconder código» para no 154 | tenerlo a la vista, se trata de hacer olvidar a uno mismo parte de la 155 | complejidad de la tarea que está realizando. 156 | 157 | Como ejemplo, sigamos con el caso anterior de los precios y los artículos. Una 158 | de las operaciones que hacemos es calcular un tanto por ciento. Si suponemos 159 | que es una operación que usaremos más de una vez, podría tener sentido abstraer 160 | ese cálculo. No nos vamos a ahorrar líneas de código, pero esta versión puede 161 | requerir menos esfuerzo mental y ser más fácil de leer (imagínese el siguiente 162 | ejemplo en castellano, tal y como hicimos en el anterior apartado): 163 | 164 | [source,lisp] 165 | .Solución abstrayendo el cálculo de porcentajes (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/functional-languages/calculate-prices.lisp[calculate-prices.lisp]) 166 | ---------------------------------- 167 | ; En el caso de Lisp, podríamos haber llamado a esta función '%', de modo que 168 | ; la llamada de abajo quedaría '(% percent-tax total-price)' 169 | (defun percentage (percent amount) 170 | (* amount (/ percent 100))) 171 | 172 | (defun calculate-price (cost nitems limit percent-tax) 173 | (let* ((total-price (* cost nitems)) 174 | (tax (percentage percent-tax total-price))) 175 | (if (> total-price limit) 176 | (+ total-price tax) 177 | total-price))) 178 | ---------------------------------- 179 | 180 | Calcular un tanto por ciento es trivial, y por escribir la función +percentage+ 181 | no estamos ahorrando líneas de código, pero cada segundo que ahorramos en 182 | entender trivialidades al leer la fuente es un segundo más que podemos dedicar 183 | a asuntos más importantes. Y el tiempo que necesitamos para entender código sin 184 | las abstracciones apropiadas, con frecuencia crece exponencialmente, no 185 | linealmente, al añadir nuevas faltas de abstracción. 186 | 187 | Otra ventaja de abstraer funciones de esta manera es que estas funciones 188 | normalmente son bastante fáciles de probar, porque tienden a tener interfaces 189 | sencillas y responsabilidades claras. En el caso de lenguajes que tienen una 190 | consola interactiva (como Lisp, Python, Ruby y otros) es fácil experimentar con 191 | la función y ver lo que hace, facilitando la escritura de pruebas unitarias en 192 | cualquier lenguaje. Especialmente si evitamos los efectos colaterales, como 193 | veremos en el siguiente apartado. 194 | 195 | 196 | Efectos colaterales 197 | ------------------- 198 | Los llamados _efectos colaterales_ son uno de los conceptos más importantes de 199 | la programación funcional, por no decir que el más importante. Es lo que 200 | diferencia los lenguajes puramente funcionales de los lenguajes funcionales no 201 | puros. Incluso los programadores de los lenguajes que no son puramente 202 | funcionales (como Lisp) generalmente intentan evitar efectos colaterales. 203 | 204 | Un efecto colateral es cualquier cambio que una función produce fuera del 205 | ámbito de la función en sí. Por ejemplo, una función que modifique una variable 206 | que ha recibido como parámetro (es decir, «parámetros de entrada/salida») o que 207 | modifique variables globales o cualquier otra cosa que no sean variables 208 | locales a la función está produciendo efectos colaterales. Esto incluye 209 | cualquier tipo de entrada/salida, como leer o escribir ficheros o interactuar 210 | con la pantalla, el teclado o el ratón. 211 | 212 | ¿Por qué es tan importante evitar efectos colaterales? De nuevo, como en el 213 | caso de las pequeñas funciones que suban el nivel de abstracción, evitar un 214 | solo efecto colateral no es una ventaja muy grande. Sin embargo, evitar efectos 215 | colaterales como regla general hace que los programas sean más fáciles de 216 | entender y mantener, y que haya menos sorpresas. La razón es que evitar efectos 217 | colaterales _garantiza_ que ningún error en la función pueda afectar a nada 218 | más. Si además no hacemos referencia a nada externo a la función, como 219 | variables globales, tenemos una garantía extra importantísima: la función es 220 | independiente del resto del código, lo que significa que ningún fallo del resto 221 | del programa puede afectar a nuestra función, y que podemos probar la función 222 | independientemente del resto del código, lo cual no sólo es práctico, sino que 223 | hace más fácil asegurarse de que cubrimos todos los casos posibles de la 224 | función con baterías de pruebas. 225 | 226 | Veamos un ejemplo de efectos colaterales en Python. El método +sort+, 227 | desgraciadamente, modifica la lista sobre la que se llama. Esto puede 228 | producir sorpresas desagradables, como veremos en el primer ejemplo. Digamos 229 | que estamos escribiendo un programa para gestionar competiciones de carreras y 230 | escribimos una función +best_time+ que recibe una lista de números y devuelve 231 | el menor (obviamos la existencia de la función +min+ para hacer el ejemplo más 232 | ilustrativo): 233 | 234 | [source,python] 235 | .Sorpresa desagradable debida a un efecto colateral (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/functional-languages/best-time-bad.py[best-time-bad.py]) 236 | ---------------------------------- 237 | def best_time_BAD(list): 238 | if len(list) == 0: 239 | return None 240 | list.sort() 241 | return list[0] 242 | 243 | times = [5, 9, 4, 6, 10, 8] 244 | best_time_BAD(times) # Devuelve 4 245 | print times # ¡Esto imprime «[4, 5, 6, 8, 9, 10]»! 246 | ---------------------------------- 247 | 248 | Una forma de resolver este problema es usar la función +sorted+ en vez del 249 | método +sort+: 250 | 251 | [source,python] 252 | .Mejor implementación, sin efectos colaterales (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/functional-languages/best-time-bad.py[best-time-bad.py]) 253 | ---------------------------------- 254 | def best_time(list): 255 | if len(list) == 0: 256 | return None 257 | return sorted(list)[0] 258 | 259 | times = [5, 9, 4, 6, 10, 8] 260 | best_time(times) # Devuelve 4 261 | print times # Imprime «[5, 9, 4, 6, 10, 8]» 262 | ---------------------------------- 263 | 264 | En Ruby normalmente se usa la convención de añadir un «!» al final del nombre 265 | del método si éste produce efectos colaterales (otra convención que se puede 266 | apreciar en el ejemplo es cómo los métodos que devuelven verdadero/falso 267 | terminan en «?»). El ejemplo de arriba se podría traducir a Ruby de la 268 | siguiente manera: 269 | 270 | [source,ruby] 271 | .Efectos colaterales en Ruby (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/functional-languages/best-time.rb[best-time.rb]) 272 | ---------------------------------- 273 | require 'pp' # Pretty printer 274 | 275 | def best_time_BAD(list) 276 | if list.empty? 277 | nil 278 | else 279 | list.sort! # «sort!», ¡con efectos colaterales! 280 | list[0] 281 | end 282 | end 283 | 284 | times = [5, 9, 4, 6, 10, 8] 285 | best_time_BAD(times) # Devuelve 4 286 | pp times # Imprime «[4, 5, 6, 8, 9, 10]» 287 | 288 | def best_time(list) 289 | if list.empty? 290 | nil 291 | else 292 | list.sort[0] # «sort», sin «!» 293 | end 294 | end 295 | 296 | times2 = [5, 9, 4, 6, 10, 8] 297 | best_time(times2) # Devuelve 4 298 | pp times2 # Imprime «[5, 9, 4, 6, 10, 8]» 299 | ---------------------------------- 300 | 301 | Por último, evitar efectos colaterales permite a las funciones usar una técnica 302 | de optimización llamada «memorización» (_memoization_ en inglés). Esta 303 | técnica consiste en recordar el valor retornado por la función la primera vez 304 | que se llama. Cuando se vuelve a llamar a la función con los mismos 305 | parámetros, en vez de ejecutar el cuerpo de la función, se devuelve el valor 306 | recordado. Si la función no produce ningún efecto colateral, esta técnica es 307 | perfectamente segura porque está garantizado que los mismos parámetros de 308 | entrada siempre producen el mismo resultado. Un ejemplo muy sencillo de 309 | memorización en Javascript es la siguiente implementación de la serie de 310 | Fibonacci: 311 | 312 | [source,javascript] 313 | .Implementación de la serie de Fibonacci con memorización (https://github.com/emanchado/camino-mejor-programador-codigo/blob/master/functional-languages/fibonacci.js[fibonacci.js]) 314 | ---------------------------------- 315 | var fibonacciCache = {0: 1, 1: 1}; 316 | 317 | function fibonacci(pos) { 318 | if (pos < 0) { 319 | throw "La serie de Fibonacci sólo está definida para números naturales"; 320 | } 321 | 322 | if (! fibonacciCache.hasOwnProperty(pos)) { 323 | console.log("Calculo el resultado para la posición " + pos); 324 | fibonacciCache[pos] = fibonacci(pos - 1) + fibonacci(pos - 2); 325 | } 326 | 327 | return fibonacciCache[pos]; 328 | } 329 | ---------------------------------- 330 | 331 | Si se copia este código en una consola Javascript (digamos, Node) y se hacen 332 | distintas llamadas a la función +fibonacci+, se podrá comprobar (gracias a los 333 | mensajes impresos por +console.log+) que cada posición de la serie sólo se 334 | calcula una vez. 335 | 336 | En lenguajes dinámicos como Python, Ruby o Javascript, es relativamente 337 | sencillo escribir una función que reciba otra función como parámetro y le 338 | aplique la técnica de «memorización». El siguiente apartado explora la técnica 339 | de manipular funciones como datos. 340 | 341 | 342 | Funciones de orden superior 343 | --------------------------- 344 | Otra de las características comunes de los lenguajes funcionales es tratar a 345 | las funciones como «ciudadanos de primera clase». Es decir, las funciones son 346 | valores más o menos normales que se pueden pasar como parámetros, asignar a 347 | variables y devolver como resultado de la llamada a una función. Las funciones 348 | que utilizan esta característica, es decir, que manipulan o devuelven 349 | funciones, reciben el nombre de _funciones de orden superior_. 350 | Afortunadamente, muchos lenguajes populares tienen este tipo de funciones. 351 | 352 | La primera vez que uno se encuentra funciones de orden superior puede pensar 353 | que sus usos son limitados, pero realmente tienen muchas aplicaciones. Por un 354 | lado, tenemos las funciones y métodos que traiga el lenguaje de serie, por lo 355 | general de manejo de listas. Por otro, tenemos la posibilidad de escribir 356 | nuestras propias funciones y métodos de orden superior, para separar o 357 | reutilizar código de manera más efectiva. 358 | 359 | Veamos un ejemplo de lo primero en Ruby. Algunos de los métodos de la clase 360 | +Array+ reciben una función como parámetro (en Ruby se los llama _bloques_), lo 361 | que permite escribir código bastante compacto y expresivo: 362 | 363 | [source,ruby] 364 | .Métodos de orden superior en Ruby 365 | ---------------------------------- 366 | # Comprobar si todas las palabras tienen menos de 5 letras 367 | if words.all? {|w| w.length < 5 } 368 | # ... 369 | end 370 | 371 | # Comprobar si el cliente tiene algún envío pendiente 372 | if customer.orders.any? {|o| not o.sent? } 373 | # ... 374 | end 375 | 376 | # Obtener las asignaturas suspendidas por un alumno 377 | failed_subjects = student.subjects.find_all {|s| s.mark < 5 } 378 | 379 | # Dividir los candidatos entre los que saben más de 380 | # dos idiomas y los demás 381 | polyglots, rest = cands.partition {|c| c.languages.length > 2 } 382 | 383 | # Obtener una versión en mayúsculas de las palabras 384 | # de la lista 385 | words = ["hoygan", "kiero", "hanime", "gratix"] 386 | shouts = words.map {|w| w.upcase} 387 | ---------------------------------- 388 | 389 | El código equivalente que habría que escribir para conseguir el mismo resultado 390 | sin funciones de orden superior es bastante más largo y difícil de leer. 391 | Además, si quisiéramos hacer operaciones parecidas variando la condición 392 | (digamos, en una parte del código queremos comprobar si todas las palabras 393 | tienen menos de cinco letras, y en otra queremos comprobar si todas las 394 | palabras se componen exclusivamente de letras, sin números u otros caracteres) 395 | el código empeoraría rápidamente. 396 | 397 | Escribir nuestras propias funciones tampoco tiene que ser difícil, ni usarse en 398 | casos muy especiales. Pueden ser usos tan comunes y sencillos como el siguiente 399 | ejemplo en Javascript: 400 | 401 | [source,javascript] 402 | .Funciones de orden superior en Javascript 403 | ---------------------------------- 404 | // Queremos poder escribir el siguiente código 405 | var comicCollection = new Database('comics'); 406 | comicCollection.onAdd(function(comic) { 407 | console.log("Nuevo cómic añadido: " + comic.title); 408 | }); 409 | // La siguiente línea debería imprimir «Nuevo cómic...» en la consola 410 | comicCollection.add({title: "Batman: The Dark Knight Returns", 411 | author: "Frank Miller"}); 412 | 413 | // La implementación de onAdd puede ser muy sencilla 414 | Database.prototype.onAdd = function(f) { 415 | this.onAddFunction = f; 416 | } 417 | // La implementación de add también 418 | Database.prototype.add = function(obj) { 419 | this.data.push(obj); 420 | if (typeof(this.onAddFunction) === 'function') { 421 | this.onAddFunction(obj); 422 | } 423 | } 424 | ---------------------------------- 425 | 426 | A partir de Ecmascript 5, la clase +Array+ añade varios métodos de orden 427 | superior que son comunes en la programación funcional. 428 | 429 | 430 | Evaluación perezosa 431 | ------------------- 432 | La última característica de lenguajes funcionales que exploraremos es la 433 | _evaluación perezosa_. No hay muchos lenguajes que incluyan evaluación 434 | perezosa, pero se puede imitar hasta cierto punto, y saber cómo funciona puede 435 | darnos ideas e inspirarnos a la hora de diseñar nuestros propios sistemas. Uno 436 | de los relativamente pocos lenguajes que incluye evaluación perezosa es 437 | Haskell. 438 | 439 | La evaluación perezosa consiste en no hacer cálculos que no sean necesarios. 440 | Por ejemplo, digamos que escribimos una función que genere recursivamente una 441 | lista de 10 elementos, y otra función que llame a la primera, pero que sólo use 442 | el valor del cuarto elemento. Cuando se ejecute la segunda función, Haskell 443 | ejecutará la primera hasta que el cuarto elemento sea calculado. Es decir: 444 | Haskell no ejecutará, como la mayoría de los lenguajes, la primera función 445 | hasta que _devuelva_ su valor (una lista de 10 elementos); sólo ejecutará la 446 | función hasta que se _genere_ el cuarto elemento de la lista, que es lo único 447 | que necesita para continuar la ejecución del programa principal. En este 448 | sentido, la primera función es como una expresión matemática: inicialmente 449 | Haskell no conoce el valor de la expresión, y sólo calculará la parte de ésta 450 | que necesite. En este caso, los cuatro primeros elementos. 451 | 452 | ¿Cuál es la ventaja de la evaluación perezosa? En la mayoría de los casos, 453 | eficiencia. En otros casos, legibilidad. Cuando no tenemos que preocuparnos por 454 | la memoria o ciclos de CPU usados por nuestra función, podemos hacer que 455 | devuelvan (teóricamente) listas o estructuras infinitas, las cuales pueden ser 456 | más fáciles de leer o implementar en algunos casos. Aunque no es el ejemplo 457 | más claro de legibilidad de evaluación perezosa, entender la siguiente 458 | implementación de la serie de Fibonacci, aclarará la diferencia con la 459 | evaluación estricta. Nótese que la función calcula la serie _entera_, es decir, 460 | una lista _infinita_: 461 | 462 | [source,haskell] 463 | .Implementación de la serie de Fibonacci, en Haskell 464 | fibs = 0 : 1 : zipWith (+) fibs (tail fibs) 465 | 466 | Normalmente la función es imposible de entender de un primer vistazo si no 467 | estamos familiarizados con la programación funcional y la evaluación perezosa, 468 | pero hay varios puntos que nos ayudarán: 469 | 470 | 1. +tail lista+ devuelve la lista dada, saltándose el primer elemento. Es 471 | decir, si +lista+ es +(1 2 3)+, +tail lista+ es +(2 3)+. 472 | 2. +zipWith+ calcula, dada una operación y dos listas, una lista que tiene: 473 | como primer elemento, el resultado de aplicar la operación dada al primer 474 | elemento de las dos listas; como segundo el resultado de aplicar la operación 475 | al segundo elemento de las dos listas; etc. Así, +zipWith+ llamado con la 476 | función suma y las listas +(1 2 3)+ y +(0 1 5)+ resultaría en +(1 3 8)+. 477 | 3. Cada elemento de la lista devuelta por +fibs+ se calculará individualmente, 478 | y estará disponible en memoria sin necesidad de volver a ejecutar el código de 479 | la función. 480 | 481 | Así, lo que ocurre es: 482 | 483 | 1. Haskell empieza a construir una lista con los elementos +0+ y +1+. En este 484 | punto, +fibs = (0 1)+. 485 | 2. El tercer elemento será el primer elemento de la subexpresión +zipWith ...+. 486 | Para calcularlo, necesitamos la lista +fibs+ (por ahora +(0 1)+, ya que sólo 487 | conocemos dos elementos) y +tail fibs+ (por ahora +(1)+). Al sumar el primer 488 | elemento de cada una de esas listas (+0+ y +1+), el resultado es +1+. En este 489 | punto, +fibs = (0 1 1)+ y la subexpresión +zipWith ... = (1)+. 490 | 3. El cuarto elemento de +fibs+ es el segundo elemento de +zipWidth ...+. Para 491 | calcularlo necesitaremos el segundo elemento de +fibs+ y el segundo elemento de 492 | +tail fibs+. El segundo elemento de +tail fibs+ es el tercer elemento de 493 | +fibs+, que ya conocemos porque lo calculamos en el paso anterior. Nótese que 494 | _no_ hace falta ninguna llamada recursiva porque los valores que necesitamos ya 495 | están calculados. La evaluación perezosa funciona como una función matemática: 496 | no hace falta que volvamos a calcular un valor si ya sabemos el resultado. En 497 | este punto, +fibs = (0 1 1 2)+ y la subexpresión +zipWith ... = (1 2)+. 498 | 4. Para el quinto elemento (el tercero de +zipWidth+), necesitaremos el tercer 499 | y cuarto elementos de +fibs+, que llegados a este punto ya conocemos porque 500 | los hemos calculado en los pasos anteriores. Y así sucesivamente. 501 | 502 | Estos pasos no se ejecutan indefinidamente: se irán ejecutando hasta que se 503 | obtenga el elemento de +fibs+ que se necesite. Es decir, si asignamos +fibs+ a 504 | una variable pero nunca la usamos, el código no se ejecutará en absoluto; si 505 | usamos el valor del tercer elemento de la serie en algún cálculo, sólo se 506 | ejecutarán los dos primeros pasos descritos arriba; etc. En ningún caso se 507 | intenta ejecutar +fibs+ hasta que devuelva «el valor completo». 508 | 509 | La evaluación perezosa se puede ver como aplicar la técnica de «memorización» 510 | automáticamente a todo el lenguaje. Un posible uso es calcular tablas de 511 | valores que son lentos de calcular: en algunos casos podríamos cargar una tabla 512 | precalculada en memoria, pero el coste puede ser prohibitivo si la tabla es 513 | grande o potencialmente infinita. 514 | 515 | 516 | Conclusión 517 | ---------- 518 | Aprender lenguajes nuevos, especialmente de paradigmas con los que estamos 519 | menos familiarizados, nos puede enseñar muchas cosas sobre programación en 520 | general. Este proceso de aprendizaje nos hará mejores programadores, y muchas 521 | de esas lecciones serán aplicables a todos los lenguajes que conozcamos, no 522 | sólo a los lenguajes similares al que acabemos de aprender. En particular, los 523 | lenguajes funcionales son suficientemente accessibles y similares a los 524 | lenguajes más populares como para enseñarnos muchas lecciones útiles. 525 | -------------------------------------------------------------------------------- /tdd.asc.in: -------------------------------------------------------------------------------- 1 | Desarrollo dirigido por pruebas 2 | =============================== 3 | Joaquín Caraballo 4 | 5 | El desarrollo dirigido por pruebas (o TDD por sus siglas en inglés) consiste en escribir la prueba que demuestra una característica del software antes de la característica en sí; paso a paso, se va acrecentando una batería de pruebas con el código para explotación, al ritmo de: 6 | 7 | ______________________________ 8 | rojo - verde - refactorización 9 | ______________________________ 10 | 11 | Donde en el paso rojo escribimos una prueba que el software no satisface, en el verde modificamos el código de explotación para que la satisfaga y en el de refactorización mejoramos el código sin cambiar funcionalidad, con la confianza de que la batería de pruebas que hemos ido creando hasta el momento nos protege de estropear algo sin darnos cuenta. 12 | 13 | Ejemplo 14 | ------- 15 | Probablemente la forma más fácil de explicar TDD es con un ejemplo a nivel unitario. Supongamos que nuestra aplicación necesita convertir millas a kilómetros; empezamos con una prueba: 16 | 17 | Rojo 18 | ~~~~ 19 | #SNIPPET "example0/MilesToKilometersConverterTest.scala" tdd/tdd-examples/src/test/scala/org/casa/translation/example0/MilesToKilometersConverterTest.scala 9 11# 20 | 21 | A continuación, como estamos usando para los ejemplos Scala, que es un lenguaje compilado, antes de poder ejecutar las pruebas necesitamos escribir un poco más de código. Nos limitaremos a añadir el mínimo código de explotación necesario para que compile. footnote:[Si estamos usando un entorno de desarrollo, la función de _arreglo_ hará la mayor parte del trabajo por nosotros. En muchos lenguajes como Scala, el compilador nos obligará a incluir alguna implementación antes de permitirnos ejecutar. Algunos desarrolladores suelen implementar inicialmente los métodos lanzando una excepción como en el ejemplo, lo que ayuda a mantener la separación rojo-verde, ya que no se piensa en la implementación hasta el paso verde. Aunque esto pueda parecer prolijo, resulta bastante rápido de producir si tenemos preparada una plantilla en nuestro entorno que introducimos con un atajo. Otra opción es generar la implementación más sencilla que se nos ocurra --por ejemplo, devolviendo +0+ o +null+--.] 22 | 23 | [source,scala] 24 | ----------------------------------------------------------------------------- 25 | object MilesToKilometersConverter { 26 | def convert(miles: Double): Double = 27 | throw new RuntimeException("Not yet implemented") 28 | } 29 | ----------------------------------------------------------------------------- 30 | 31 | Para terminar el paso rojo, ejecutamos la prueba, comprobando que falla (lo que muchos entornos marcan en rojo) lanzando la excepción que esperábamos: 32 | 33 | ----------------------------------------------------------------------------- 34 | Not yet implemented 35 | java.lang.RuntimeException: Not yet implemented 36 | ----------------------------------------------------------------------------- 37 | 38 | Aunque este paso parezca trivial, nos ha obligado a tomar unas cuantas decisiones importantes: 39 | 40 | Primero, nos ha obligado a definir el contrato de uso del conversor; en este caso, el nombre sugiere que convierte específicamente millas a kilómetros, y no a la inversa; el método de conversión forma parte del objeto (y no de la clase) +MilesToKilometersConverter+, por lo que no es necesario crear un objeto conversor llamando a +new+, y sugiere que no tiene estado footnote:[Para los lectores más familiarizados con Java que con Scala, los objectos creados con +object+ en Scala son semejantes a la parte estática de las clases en Java; los métodos definidos bajo +object+ no están ligados a ningún ejemplar (en inglés _instance_) de la clase +MilesToKilometersConverter+ --que de hecho aquí no existe--. Lo cierto es que estos objetos sí que pueden tener estado (de la misma manera que podemos tener campos estáticos en Java), pero muchos equipos eligen no usar esta característica del lenguaje, a no ser que haya un buen motivo; por eso decimos que definir el método +convert+ en el objeto sugiere que el conversor no tiene estado.]; las millas están expresadas con tipo +Double+; etc. 41 | 42 | Segundo y más fundamental, la prueba constituye un ejemplo que comienza a definir la funcionalidad de la unidad; que al estar expresado en un lenguaje de programación nos protege de las ambigüedades del lenguaje natural. Por ejemplo, no dudaremos de la definición de milla: 43 | ______________ 44 | Cuando dijimos millas, ¿eran millas internacionales (1,609km)? ¿Millas náuticas (1,852km)? ¿Millas nórdicas (10km)? 45 | ______________ 46 | 47 | Porque la prueba ha dejado claro que el tipo de milla que importa en nuestra aplicación _es igual a 1,609 kilómetros._ 48 | 49 | Aunque a veces parezca innecesario, merece la pena, antes de progresar al paso verde, completar el rojo llegando a ejecutar una prueba que _sabemos_ que va a fallar; esto nos obliga a pensar en la interfaz que estamos creando, y con frecuencia desvela suposiciones erróneas... especialmente cuando resulta que la prueba no falla, o que lo hace de una forma distinta de la que esperábamos. 50 | 51 | Verde 52 | ~~~~~ 53 | A continuación acrecentamos el código de explotación. Lo más purista es escribir sólo el código imprescindible para superar la prueba: 54 | 55 | [source,scala] 56 | ----------------------------------------------------------------------------- 57 | object MilesToKilometersConverter { 58 | def convert(miles: Double): Double = 1.609 59 | } 60 | ----------------------------------------------------------------------------- 61 | 62 | Y ejecutar la prueba, que nuestra implementación superará con un color verde, si usamos un entorno que lo represente así. 63 | 64 | 65 | ¿Refactorización? 66 | ~~~~~~~~~~~~~~~~~ 67 | En esta iteración es pronto para refactorizar. Pasamos al siguiente paso. 68 | 69 | 70 | Rojo 71 | ~~~~ 72 | Antes de saltar a la implementación general, merece la pena que consideremos otras condiciones de entrada: ¿aceptamos distancias negativas? Como en este caso sí las aceptamos, lo expresamos con otra prueba. 73 | 74 | #SNIPPET "example0/MilesToKilometersConverterTest.scala (negativos)" tdd/tdd-examples/src/test/scala/org/casa/translation/example0/MilesToKilometersConverterTest.scala 13 15# 75 | 76 | Ejecutamos las pruebas para ver como la nueva falla. 77 | 78 | ----------------------------------------------------------------------------- 79 | 1.609 was not equal to -3.218 80 | org.scalatest.TestFailedException: 1.609 was not equal to -3.218 81 | ----------------------------------------------------------------------------- 82 | 83 | Verde 84 | ~~~~~ 85 | Ahora que tenemos dos ejemplos de la funcionalidad de +convert+, es un buen momento para buscar una implementación más general footnote:[A esta generalización Kent Beck la llama http://www.informit.com/articles/article.aspx?p=30641[_triangulación_]. No estoy seguro de que me guste el término, porque la triangulación geométrica a la que hace analogía permite de forma determinista encontrar una posición a partir de los datos de que se dispone. Aquí, sin embargo, los ejemplos por sí solos no nos permitirían encontrar la solución general, que precisa que además entendamos el problema más allá de los ejemplos.] que no sólo satisfaga estas dos pruebas, sino que también nos proporcione la funcionalidad general de la que son ejemplo. Como la siguiente: 86 | 87 | #SNIPPET "example0/MilesToKilometersConverter.scala" tdd/tdd-examples/src/main/scala/org/casa/translation/example0/MilesToKilometersConverter.scala 3 5# 88 | 89 | Podríamos haber pasado a la solución general directamente, y a menudo haremos exactamente eso, sin embargo, como vimos en el anterior paso verde, empezar por la solución más simple y dejar la generalización para el siguiente ciclo, nos ayuda a centrarnos primero en definir qué funcionalidad vamos a añadir y cuál va a ser el contrato de la unidad que estamos probando, resolviendo ambigüedades y desvelando suposiciones erróneas. Esto será particularmente útil cuando no tengamos claro lo que queremos hacer y cómo queremos implementarlo. 90 | 91 | 92 | Refactorización 93 | ~~~~~~~~~~~~~~~ 94 | Tras cada paso verde debemos plantearnos la mejora del código, con la confianza de que las pruebas nos van a proteger de romper algo que funcionaba anteriormente. Las refactorizaciones pueden centrarse en el detalle (¿es mejor expresar +miles * 1.609+ o +1.609 * miles+? ¿el parámetro debería llamarse +miles+ o +distanceInMiles+?), pero es fundamental que con frecuencia también reconsideremos el diseño de la aplicación y lo transformemos en lo más apropiado para el estado actual del código, sin ignorar la dirección futura en la que queremos ir pero sin fraguar demasiado lo que puede que nunca necesitemos. 95 | 96 | 97 | Por qué y para qué 98 | ------------------ 99 | El motivo más importante footnote:[_Para mí_ el más importante, seguro que otros discreparán.] para desarrollar dirigido por pruebas es el énfasis en la función de nuestro código y en su idoneidad para ponerlo a prueba y por lo tanto usarlo. Este énfasis nos obliga a preguntarnos cada vez que vamos a añadir código de explotación: ¿es esto lo que necesito? ¿hace falta _ahora_?; ayudándonos a escribir exclusivamente lo que necesitamos y a mantener el tamaño y la complejidad del código al mínimo necesario. 100 | 101 | Si bien TDD no exime de buscar y dirigir el diseño deliberadamente, escribir código que, desde el primer momento, es fácil de probar favorece una cierta simplicidad y, definitivamente, evidencia el acoplamiento, guiándonos hacia el cuidado de la colaboración entre las unidades. En particular, la inyección de dependencias y la separación entre sus interfaces y las implementaciones, emergen de forma natural, dado que facilitan las pruebas automatizadas. 102 | 103 | Los proyectos que se desarrollan dirigidos por pruebas cuentan en todo momento con una batería de pruebas al día, que documenta la intención de cada unidad del software, de combinaciones de unidades y del software en su totalidad. Además, las pruebas, si bien no la garantizan, dan una buena indicación de la corrección del software; lo que reduce el miedo a romper algo, y lo sustituye por un hábito diligente de refactorizar con frecuencia y mejorar el diseño progresivamente. 104 | 105 | Ejemplo de cómo las pruebas nos guían respecto a la cohesión y el acoplamiento 106 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 107 | En la clase siguiente, el método +translate+ traduce un texto del español a una especie de inglés, mostrando por pantalla el resultado; este código es poco probable que haya sido desarrollado guiado por pruebas, dado que el resultado principal, la traducción, no se pone a disposición del codigo externo de ninguna manera que sea fácil de incluir en una prueba, sino que se manifiesta llamando al método +println+ que da acceso a la consola, es decir, mediante un _efecto secundario_, lo que dificulta la verificación desde una prueba. 108 | 109 | #SNIPPET "example1coupled/SpanishIntoEnglishTranslator.scala" tdd/tdd-examples/src/main/scala/org/casa/translation/example1coupled/SpanishIntoEnglishTranslator.scala 3 11# 110 | 111 | Si lo desarrollamos con la facilidad de prueba en mente desde el principio, probablemente nos encontraremos con que, para probar el resultado de la traducción, necesitamos que el código que traduce devuelva el resultado; de hecho, ¿acaso no es la traducción en sí la responsabilidad principal de esta clase, y no el mostrar por pantalla? Si pudiéramos obtener el resultado, una prueba de nuestro traductor podría ser algo así: 112 | 113 | #SNIPPET "example2/SpanishIntoEnglishTranslatorTest.scala" tdd/tdd-examples/src/test/scala/org/casa/translation/example2/SpanishIntoEnglishTranslatorTest.scala 9 21# 114 | 115 | Lo que nos llevaría a un traductor menos acoplado a la muestra por pantalla 116 | 117 | #SNIPPET "example2/SpanishIntoEnglishTranslator.scala" tdd/tdd-examples/src/main/scala/org/casa/translation/example2/SpanishIntoEnglishTranslator.scala 3 10# 118 | 119 | Probar el conjunto de la aplicación 120 | ----------------------------------- 121 | Hasta ahora nos hemos centrado en las pruebas unitarias; no obstante, si somos consecuentes con los principios que hemos visto --guiarnos manteniendo el enfoque en los objetivos del software, documentar y verificar--, deberemos considerar fundamental guiar el desarrollo de cada parte de la funcionalidad mediante una prueba que la ejercite en su conjunto; lo ideal será que todas las pruebas funcionales verifiquen el conjunto del software, en un entorno similar al de explotación, o incluso en el entorno de explotación en sí. En la práctica suele haber muchos obstáculos, por ejemplo, puede que sea demasiado costoso llevar a cabo _de verdad_ ciertas acciones destructivas, que no haya suficientes recursos, o que se hayan impuesto decisiones arquitectónicas que dificulten las pruebas; sin embargo, eso no significa que tengamos que claudicar completamente; a menudo, lograremos las mejoras más importantes en el software y en la organización en la que se crea cuestionando las limitaciones. 122 | 123 | Ejemplo 124 | ~~~~~~~ 125 | Volvamos al ejemplo inicial de conversión de distancias, y supongamos que necesitamos ofrecer a nuestros clientes un servicio de conversión de unidades a través de un servicio web, porque hemos decidido que no hay suficientes conversores en Internet. La _primera_ prueba que vamos a escribir, incluso antes de las que vimos en la introducción, es una prueba que ejercite el conjunto de la aplicación. Nos concentraremos en un cierto mínimo incremento de funcionalidad, visible para los usuarios del sistema, que requiera una implementación reducida y que tenga un alto valor desde un punto de vista comercial o de los objetivos últimos del proyecto. En nuestro caso empezamos con la conversión de millas a kilómetros. 126 | 127 | #SNIPPET "step1/functional/ConversionTest.scala" tdd/unitconvert/src/test/scala/org/casa/unitconvert/step1/functional/ConversionTest.scala 9 29# 128 | 129 | El método +get+ es aquí un método de ayuda para pruebas, que hace una petición _HTTP get_ y devuelve en contenido del cuerpo del mensaje. Evidentemente, poner esto en funcionamiento requiere un cierto trabajo, pero si nos concentramos en lo fundamental, no será tanto y además nos ayudará a plantearnos cuestiones importantes acerca del sistema, particularmente a nivel de aplicación, por ejemplo: _¿cómo nos comunicaremos con el sistema?_; y acerca de cómo lo vamos a probar. Así, desde el primer momento la facilidad de prueba es un _usuario de pleno derecho_ de nuestro proyecto. 130 | 131 | Con esta prueba como guía, nos concentraremos ahora en recorrer todo el sistema, casi a toda velocidad, hasta que la satisfagamos. En el mundo de Java/Scala, la forma típica de resolver esto es con un Servlet. De nuevo comenzamos con una prueba, esta vez a nivel unitario. footnote:[En este ejemplo hemos usado dobles de prueba para verificar el comportamiento de +Converter+ con sus colaboradores; simplemente estamos comprobando que, cuando llamamos al método +doGet()+ pasando un objeto de tipo +HttpServletRequest+ y otro de tipo +HttpServletResponse+, el método bajo prueba llama al método +getWriter+ del segundo parámetro y, al devolver +getWriter+ un objeto de tipo +PrintWriter+, el método bajo prueba llama al método +print+ del +PrintWriter+ con el parámetro +"1.609"+. Gracias a herramientas como Mockito (<<mockito>>), que hemos usado en el ejemplo, es más fácil explicar la interacción en código que en español. En _Diseño ágil con TDD_ (<<bleyco-bis>>) se explica el uso de dobles y de otros aspectos de la programación dirigida por pruebas.] 132 | 133 | #SNIPPET "step1/ConverterTest.scala" tdd/unitconvert/src/test/scala/org/casa/unitconvert/step1/ConverterTest.scala 8 18# 134 | 135 | 136 | Un conversor más o menos mínimo usando Jetty viene a ser: 137 | 138 | #SNIPPET "step1/Converter.scala" tdd/unitconvert/src/main/scala/org/casa/unitconvert/step1/Converter.scala 7 29# 139 | 140 | Como vemos la funcionalidad que estamos ofreciendo es, como en el ejemplo inicial, trivial. Pero llegar a ella nos ha obligado a definir el esqueleto de todo nuestro sistema, incluyendo código de explotación y de prueba. 141 | 142 | A continuación progresaremos dependiendo de nuestras prioridades. Por ejemplo, podemos concentrarnos en completar funcionalmente la conversión de millas a kilómetros. 143 | 144 | #SNIPPET "step2/functional/ConversionTest.scala" tdd/unitconvert/src/test/scala/org/casa/unitconvert/step2/functional/ConversionTest.scala 16 18# 145 | 146 | #SNIPPET "step2/Converter.scala" tdd/unitconvert/src/main/scala/org/casa/unitconvert/step2/Converter.scala 7 12# 147 | 148 | A continuación el manejo de los casos de error, como cantidades de millas que no sean numéricas 149 | 150 | #SNIPPET "step3/functional/ConversionTest.scala" tdd/unitconvert/src/test/scala/org/casa/unitconvert/step3/functional/ConversionTest.scala 21 24# 151 | 152 | #SNIPPET "step3/Converter.scala" tdd/unitconvert/src/main/scala/org/casa/unitconvert/step3/Converter.scala 7 21# 153 | 154 | Además de los pasos _rojos_ y _verdes_ que hemos visto en el ejemplo hasta ahora, a medida que la aplicación va creciendo, debemos tanto refactorizar a nivel unitario como ir mejorando el diseño; por ejemplo, si en los próximos pasos la aplicación necesitase responder a distintas rutas con distintas conversiones, probablemente decidiríamos extraer el análisis de las URIs a una unidad independiente e introduciríamos distintos objetos a los que delegar dependiendo del tipo de conversión. 155 | 156 | 157 | Probar una sola cosa a la vez 158 | ----------------------------- 159 | El mantenimiento de la batería de pruebas, que crece con la aplicación, requiere una inversión de esfuerzo constante; hacer que cada prueba verifique únicamente un aspecto de la aplicación nos ayudará a mantener este esfuerzo manejable y además las hará más fáciles de entender, y por lo tanto más eficientes. Idealmente, el cambio de un detalle del funcionamiento de nuestra aplicación debería afectar exclusivamente a una prueba que sólo verifica ese detalle, o, dicho de otra manera: 160 | 161 | * si es relativamente fácil cambiar un cierto aspecto del funcionamiento sin que falle ninguna prueba, tenemos una laguna en la cobertura de la batería; footnote:[El tipo de pruebas con las que guiamos el desarrollo en este artículo documentan el comportamiento con buenos ejemplos, pero generalmente no lo garantizan para todas las posibles entradas, ni aspiran a hacerlo; por lo tanto, normalmente es muy fácil cambiar el comportamiento de forma intencionada sin que las pruebas dejen de satisfacerse --por ejemplo, con algo como +if(input==15) throw new EasterEgg+--; una buena cobertura en TDD está en el punto de equilibrio en el que sea poco probable cambiar la funcionalidad accidentalmente sin que fallen las pruebas.] 162 | * si falla más de una, la batería tiene código redundante, incrementando el coste de mantenimiento; 163 | * si la prueba que falla incluye la verificación de elementos que no están directamente relacionados con nuestro cambio, probablemente sea demasiado compleja, dado que introducir el cambio en el sistema requiere tener en cuenta aspectos independientes de la aplicación. 164 | 165 | Ejemplo de pruebas centradas 166 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 167 | Volvamos a donde dejamos el ejemplo del traductor y supongamos que lo siguiente que queremos hacer es separar las palabras del texto original no sólo mediante espacios, sino también mediante cambios de línea. Como estamos guiando los cambios con pruebas, añadimos a +SpanishIntoEnglishTranslatorTest+ una prueba que verifique el nuevo funcionamiento. 168 | 169 | [source,scala] 170 | ----------------------------------------------------------------------------- 171 | test("splits by change of line") { 172 | translator.translate("yo\nsoy") should be("I am") 173 | } 174 | ----------------------------------------------------------------------------- 175 | 176 | El problema que tiene esto es que la prueba mezcla la separación del texto original y la traducción de las palabras; la idea que queremos transmitir con este ejemplo estaría más clara si pudiéramos expresar la entrada como +"xxx\nxx"+ y la condición a cumplir como +should be(Seq("xxx", "xx"))+; sin embargo, la forma actual del sistema no lo permite, porque la traducción es parte del método que estamos probando. 177 | 178 | Supongamos además que el siguiente incremento funcional afectase a la traducción de palabras en sí, por ejemplo, cambiando el idioma origen al francés o a otra variante del español; este cambio afectaría a cada una de las pruebas de +SpanishIntoEnglishTranslatorTest+, pero, ¿por qué debería verse afectada una prueba como +test("splits by change of line")+, cuyo propósito es probar la separación en palabras? 179 | 180 | Podemos ver estas deficiencias de nuestra batería como el resultado de una granularidad inapropiada, dado que la misma prueba está verificando varias cosas: separación, traducción y reunión de palabras. La solución consistiría en refactorizar antes de aplicar el cambio: ¿Quizá la clase que se encarga de descomponer y componer debería ser distinta de la que traduzca palabra por palabra? 181 | 182 | Extraemos la división de palabras a su propia unidad, con lo que podemos expresar, con una prueba más centrada, la división del texto a traducir por saltos de línea: 183 | 184 | #SNIPPET "example3/SplitterTest.scala" tdd/tdd-examples/src/test/scala/org/casa/translation/example3/SplitterTest.scala 6 14# 185 | 186 | #SNIPPET "example3/Splitter.scala" tdd/tdd-examples/src/main/scala/org/casa/translation/example3/Splitter.scala 3 5# 187 | 188 | El traductor lo recibe como una dependencia inyectada mediante el constructor. footnote:[En este ejemplo nos hemos quedado con pruebas que ejercitan las unidades unidades independientemente; perdiendo la cobertura de la combinación de ambas unidades. Sin embargo, en la práctica, este código formaría parte de un proyecto mayor que contaría con baterías de pruebas de mayor ámbito, que incluirían la verificación del comportamiento conjunto.] 189 | 190 | #SNIPPET "example3/SpanishIntoEnglishTranslatorTest.scala" tdd/tdd-examples/src/test/scala/org/casa/translation/example3/SpanishIntoEnglishTranslatorTest.scala 6 22# 191 | 192 | #SNIPPET "example3/SpanishIntoEnglishTranslator.scala" tdd/tdd-examples/src/main/scala/org/casa/translation/example3/SpanishIntoEnglishTranslator.scala 3 12# 193 | 194 | El aumento de la granularidad nos ha permitido que la introducción de funcionalidad nueva no afecte a multitud de pruebas. Sin embargo, esto no ha sido gratis; hemos aumentado la complejidad del código. Al final, todas estas decisiones hay que valorarlas una a una y decidir qué es lo más apropiado en cada caso, teniendo en cuenta aspectos como la complejidad, el tiempo de ejecución y la dirección en la que esperamos y queremos que vaya el proyecto. 195 | 196 | Mantener verde la batería 197 | ------------------------- 198 | La batería de pruebas es la documentación de la funcionalidad de nuestro código. Una documentación que se mantiene al día, porque va creciendo con cada cambio y es ejercitada, es decir, ejecutamos las pruebas, al menos con cada envío de los cambios al repositorio. 199 | 200 | Trabajar dirigido por pruebas significa mantener siempre el correcto funcionamiento del sistema; idealmente la última versión en el repositorio común deberá estar en todo momento lista para ponerla en explotación, y las pruebas satisfechas en todo momento footnote:[De hecho, algunos equipos hacen exactamente eso, ponen cada versión que satisface la batería completa automáticamente en explotación.], con lo que la documentación proporcionada por las pruebas estará siempre al día. Para lograrlo, deberemos comprobar la satisfacción de las pruebas antes de enviar cualquier cambio al repositorio común; además, muchos equipos se ayudan de un sistema de integración continua que verifica automáticamente la batería cada vez que se detecta un cambio en el repositorio. 201 | 202 | A medida que crece la aplicación, el tiempo que requiere la batería completa tiende a aumentar, lo que incrementa el coste del desarrollo y motiva a los desarrolladores a no siempre satisfacer la batería completa o a espaciar los envíos de cambios al repositorio común; ambos efectos muy perniciosos. Para mantener viable la programación dirigida por pruebas, debemos esforzarnos en mantener reducido este tiempo; la manera de lograrlo va más allá de un artículo introductorio, pero incluye la selección y el ajuste de la tecnología empleada para los distintos elementos de la batería, la ejecución en paralelo e incluso la partición de la aplicación en sí o cualquier ajuste que la haga más rápida.footnote:[Otra forma de reducir el tiempo es transigiendo en alguna medida, es decir, no cumpliendo en ocasiones todo lo que describimos en este artículo.] 203 | 204 | Otro problema relacionado con el coste de la batería son los fallos intermitentes, que necesitarán un esfuerzo importante de mantenimiento; hemos de invertir el esfuerzo necesario para entender la raíz de cada fallo y resolverlo. Las fuentes típicas de fallos intermitentes son los aspectos no deterministas del software; por ejemplo, cuando lo que verificamos es de naturaleza asíncrona, necesitamos controlar el estado de la aplicación mediante puntos de sincronización. Algunas condiciones son imposibles de verificar per se, como la ausencia de comportamiento; en estos casos a menudo la solución pasa por alterar la aplicación desvelando su estado lo suficiente como para que las pruebas puedan sincronizarse con la aplicación y sepan cuándo debería haber tenido lugar el evento que estemos verificando. También suelen causar fallos intermitentes las dependencias de elementos fuera del control de la prueba, como el reloj del sistema; y su solución pasa normalmente por la inclusión y el control de dicho elemento desde la prueba, por ejemplo, alterando la percepción del tiempo de la aplicación mediante un _proxy_ controlable desde las pruebas. 205 | 206 | Críticas 207 | -------- 208 | Diseño prematuro 209 | ~~~~~~~~~~~~~~~~ 210 | El ejemplo de pruebas centradas ilustra también una de las principales críticas contra el desarrollo dirigido por pruebas: para poder probar la clase de traducción satisfactoriamente, la hemos descompuesto en un diccionario y un desensamblador/ensamblador de palabras; sin embargo, si de verdad fuéramos a diseñar un sistema de traducción automatizada esta abstracción no sería apropiada, ya que el diccionario necesita el contexto, la disposición de palabras en el texto resultante depende de la función gramatical, etc. ¿Significa esto que el TDD nos ha guiado en la dirección incorrecta? 211 | 212 | Como dijimos antes, desarrollar dirigidos por pruebas significa considerar las pruebas como usuarios de pleno derecho de nuestro código, añadiendo, así, un coste en cantidad de código y en la complejidad del diseño que elegimos pagar para beneficiarnos de la guía de las pruebas; optar por el TDD es considerar que este coste vale la pena. Sería menos costoso desarrollar sin la guía de las pruebas si supiésemos exactamente cuáles son los requisitos e incluso qué código escribir desde el principio. El TDD se opone a esta visión considerando el desarrollo como un proceso progresivo en el que el equipo va descubriendo _qué_ y _cómo_ desarrollar, a medida que crea la aplicación y la va poco a poco transformando, enriqueciendo y simplificando, pasando siempre de un sistema que funciona a un otro sistema que funciona, y que hace quizá un poco más que el anterior. 213 | 214 | Volviendo a la prematuridad del diseño, si bien es cierto que las pruebas a veces adelantan la necesidad de descomponer el código, estas decisiones se vuelven más baratas dado que los cambios en el diseños son menos costosos gracias a la protección que nos da nuestra batería. Y, además, esto se ve también compensado por las innumerables ocasiones en las que no añadiremos complejidad al código de explotación porque esa complejidad no es necesaria para satisfacer las pruebas, probablemente porque no lo es en absoluto para lo que requerimos de nuestra aplicación en ese momento. 215 | 216 | Guiar sólo mediante pruebas funcionales 217 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 218 | Algunos equipos experimentados deciden utilizar las pruebas para guiar el desarrollo a nivel funcional, pero no a nivel unitario, sólo escribiendo pruebas unitarias cuando no resulta práctico cubrir con pruebas funcionales ciertas partes del código. Esto lo hacen porque ven las pruebas unitarias más como un lastre que como una guía útil en el diseño interno de la aplicación, quizá porque consideran que su criterio es suficiente para lograr un buen diseño interno. Nuestra recomendación es comenzar guiando todas las unidades por pruebas y progresar hacia un equilibrio en el que no probemos a nivel unitario el código que sea obvio, pero donde reconozcamos que el código que no merece la pena guiar mediante pruebas por ser obvio probablemente es poco útil footnote:[En algunos casos, debido a una convención sospechosa o a las limitaciones de nuestro lenguaje de programación --por ejemplo, los métodos _set_ y _get_ en Java--.], y que el que es difícil de probar probablemente peca de acoplado. La recomendación es, en definitiva, aplicar el sentido común pero no renunciar a la guía que nos proporcionan las pruebas en el desarrollo de los niveles inferiores de la aplicación. 219 | 220 | Conclusión 221 | ---------- 222 | El desarrollo dirigido por pruebas nos ayuda a construir aplicaciones donde aquello que nos motiva a escribir el software guía cada línea de código, evitando a cada nivel desperdiciar nuestros esfuerzos en desarrollar funcionalidad que no sea exactamente la que necesitamos, o que sea simplemente innecesaria. La guía de las pruebas, combinada con el diseño continuo, hace posible un estilo de desarrollo orgánico, en el que el equipo evoluciona la aplicación, a medida que mejora su conocimiento de los requisitos del dominio y de la implementación ideal para resolverlos. Además, la utilización de pruebas desde el primer momento, desvela el acoplamiento y nos incita a descomponer el código en unidades cohesivas con depencias claramente identificadas. 223 | 224 | Si bien no hay un concenso absoluto en la idoneidad del desarrollo guiado por pruebas, muchos equipos lo usan, particularmente para aplicaciones comerciales y en lenguajes orientados a objetos. Nuestra opinión es que, además de dar sentido y ritmo a cada actividad del desarrollo, lo hace más divertido, porque da al desarrollador más libertad para mejorar y acrecentar el software. 225 | -------------------------------------------------------------------------------- /consejospruebas.asc: -------------------------------------------------------------------------------- 1 | Siete problemas al probar programas 2 | =================================== 3 | _Esteban Manchado Velázquez_ 4 | 5 | La mayoría de los profesionales de la informática coincidirán en que probar es 6 | una de las tareas fundamentales del desarrollo, pero si ya es difícil aprender 7 | técnicas de programación, mucho más difícil es aprender técnicas de pruebas, 8 | tanto manuales como automáticas. Primero, porque desgraciadamente es un 9 | conocimiento menos extendido. Segundo, porque es aún más abstracto que la 10 | programación. 11 | 12 | Por ello, todos los consejos y la experiencia compartida de los que nos podamos 13 | aprovechar son especialmente importantes. Los siguientes problemas están 14 | centrados en las pruebas automáticas, pero muchos de ellos ocurren también al 15 | hacer pruebas manuales. Están ordenados por experiencia: el primer problema 16 | aparece principalmente en equipos que nunca han escrito pruebas, y el último lo 17 | tienen incluso desarrolladores con años de experiencia. 18 | 19 | Como todo, hay que aplicar las soluciones propuestas entendiendo por qué, cómo 20 | y cuándo son aplicables y útiles, nunca siguiéndolas ciegamente en todas las 21 | situaciones. 22 | 23 | 24 | Dejar las pruebas para el final 25 | ------------------------------- 26 | Aunque es normal que al acercarse el final de cada ciclo de desarrollo se 27 | intensifique el esfuerzo de pruebas (especialmente las manuales), es un error 28 | mayúsculo no haber probado desde el principio del desarrollo. Esto no es un 29 | tópico o una consideración teórica o académica: no probar desde el principio 30 | del ciclo de desarrollo tiene muchas desventajas. Por ejemplo: 31 | 32 | 1. El esfuerzo mental de probar un programa grande es mucho mayor que el 33 | esfuerzo de probar un programa pequeño. No se sabe por dónde empezar, es 34 | difícil saber cuándo terminar, y es fácil quedarse con la sensación de que no 35 | todo está probado correctamente. Si probamos los componentes que vamos creando 36 | desde el principio, es mucho más fácil saber cómo atacar el problema y hacer 37 | pruebas más completas. 38 | 39 | 2. Si probamos mientras escribimos código y sabemos el nivel de calidad de 40 | éste, será más fácil hacer estimaciones sobre los recursos necesarios para 41 | terminar el resto del trabajo. Esto nos dará mucha más flexibilidad para 42 | renegociar fechas o las características que se tendrán que quedar fuera, en vez 43 | de pasar al estado de alarma cuando nos demos cuenta, demasiado tarde, de que 44 | no tenemos tiempo de arreglar los fallos que encontremos los últimos días. 45 | 46 | 3. Cuando hemos «terminado» un proyecto y sólo queda probar, es normal tener la 47 | tendencia a «no querer ver fallos» o restarles importancia, a que nos inunde 48 | un falso optimismo que confirme que de verdad hemos terminado y no queda nada 49 | por hacer. Sobre todo si quedan pocos días hasta la entrega y sentimos que no 50 | podemos hacer gran cosa, y sólo queremos entregar el resultado y olvidarnos del 51 | proyecto. Esto ocurre mucho menos si tenemos a profesionales exclusivamente 52 | dedicados a las pruebas, claro. 53 | 54 | 4. Probar desde el principio significa que estaremos probando durante más 55 | tiempo, por lo que habremos encontrado más casos que puedan ser problemáticos, 56 | y por tanto más fallos (y las soluciones a éstos), antes de que sea demasiado 57 | tarde. 58 | 59 | 5. Todas las pruebas que hagamos pronto ayudarán a aumentar la calidad del 60 | código, lo que no sólo afecta a la calidad final del producto, sino a lo fácil 61 | que es escribir y depurar cualquier otro código que dependa del primero. Los 62 | fallos de interacción entre las dos bases de código serán más fáciles de 63 | encontrar y, en particular, la fuente de los errores. 64 | 65 | 6. Si desde el principio escribimos pruebas automáticas, nos obligaremos a 66 | nosotros mismos a escribir APIs más limpias. Generalmente, código más fácil de 67 | probar es código más desacoplado, más limpio y más estable. Indudablemente, una 68 | de las ventajas de escribir pruebas automáticas es que ayudan a mantener un 69 | diseño de mayor calidad. Pero si no probamos desde el principio, perdemos esta 70 | ventaja. 71 | 72 | 7. Al no probar desde el principio, desarrollaremos código que no es 73 | especialmente fácil de probar, y cuanto más tarde empecemos a adaptar el código 74 | para que lo sea, más esfuerzo costará. Una vez hayamos escrito una gran 75 | cantidad de código sin tener en cuenta si es fácil de probar o no, la tentación 76 | de dejar muchas partes sin pruebas será irresistible. Y, por supuesto, si no 77 | hacemos el esfuerzo de adaptar el código en ese momento, entraremos en un 78 | círculo vicioso. 79 | 80 | Como tarde o temprano se tendrá que probar el resultado del trabajo, mejor 81 | empezar temprano porque es menos costoso (en tiempo y esfuerzo mental) y los 82 | resultados son mejores. Escribir código sin probar es simplemente irresponsable 83 | y una falta de respeto con respecto a los usuarios del producto _y_ al resto de 84 | los miembros del equipo, especialmente los que tengan que mantener el código 85 | después y cualquier miembro del equipo que use directa o indirectamente el 86 | código sin probar. 87 | 88 | 89 | Ser demasiado específico en las comprobaciones 90 | ---------------------------------------------- 91 | Esto es un problema bastante común, sobre todo cuando se empiezan a hacer 92 | pruebas. Las pruebas automáticas son, de alguna manera, una descripción de lo 93 | que se espera que el programa haga. Una especificación en código, por así 94 | decirlo. Como tal, sólo debe describir el comportamiento que esperamos que no 95 | cambie. Si somos demasiado específicos o exigentes en las comprobaciones de 96 | nuestras pruebas, éstas no sólo evitarán que introduzcamos fallos en el código, 97 | sino también que podamos hacer otro tipo de cambios. 98 | 99 | Por ejemplo, digamos que estamos escribiendo una aplicación de tareas 100 | pendientes muy sencilla. La clase que representa una lista de tareas se llama 101 | BrownList, y podría tener una pinta así en Ruby: 102 | 103 | [source,ruby] 104 | .Ejemplo de implementación de la clase +BrownList+ 105 | ---------------------------------- 106 | class BrownList 107 | def initialize(db) 108 | # ... 109 | end 110 | 111 | def add_brown(title) 112 | # Insertamos en una base de datos, devolvemos un id 113 | end 114 | 115 | def mark_brown_done(id) 116 | # Marcamos el id dado como hecho 117 | end 118 | 119 | def pending_browns 120 | # Devolvemos una lista de las tareas pendientes 121 | end 122 | end 123 | 124 | # Se usa así 125 | bl = BrownList.new(db_handle) 126 | id = bl.add_brown("Tarea pendiente") 127 | bl.mark_brown_done(id) 128 | lista_tareas_pendientes = bl.pending_browns 129 | ---------------------------------- 130 | 131 | Ahora, para probar que el método +add_brown+ funciona, puede que se nos ocurra 132 | conectarnos a la base de datos y comprobar si tiene la fila correcta. En la 133 | gran mayoría de los casos esto es un error. Para entender por qué, hay que 134 | darse cuenta de que las pruebas definen _qué_ significa que el código funcione. 135 | Así, si las pruebas usan detalles de la implementación, se estará definiendo de 136 | manera implícita que el programa sólo «funciona bien» si mantiene los mismos 137 | detalles de implementación. Lo cual es, naturalmente, un error, porque no 138 | permite que el código evolucione. 139 | 140 | [source,ruby] 141 | ._Mal_ ejemplo de prueba del método +add_brown+ de +BrownList+ 142 | ---------------------------------- 143 | class TestBrownList < Test::Unit::TestCase 144 | def setup 145 | # Recreamos la base de datos de pruebas para que esté vacía 146 | end 147 | 148 | def test_add_brown_simple_____MAL 149 | db_handle = ... 150 | bl = BrownList.new(db_handle) 151 | bl.add_brown("Tarea de prueba") 152 | 153 | count = db_handle.execute("SELECT COUNT(*) FROM browns") 154 | assert_equal 1, count 155 | end 156 | end 157 | ---------------------------------- 158 | 159 | En este ejemplo concreto, hay muchos casos en los que esta prueba fallaría, a 160 | pesar de que el código podría estar funcionando perfectamente: 161 | 162 | * El nombre de la tabla donde guardamos las tareas cambia 163 | * Añadimos la característica multiusuario, por lo que podría haber más tareas 164 | en esa tabla de las que queremos contar 165 | * Decidimos que vamos a usar una base de datos no-SQL, por lo que la manera de 166 | contar cambiaría 167 | * Añadimos un paso intermedio de algún tipo, de tal manera que las tareas no se 168 | crearían inicialmente en la base de datos, sino en algo como _memcached_, y 169 | unos segundos después irían a la base de datos 170 | 171 | Las pruebas no deben limitarnos cuando reorganizamos código o cambiamos 172 | detalles de implementación. De hecho, una de las ventajas de tener pruebas 173 | automáticas es que cuando reorganicemos código, sabremos si estamos haciendo 174 | algo mal porque las pruebas fallarán. Si no estamos seguros de que cuando una 175 | prueba falla es porque hay un problema en el código, nuestras pruebas no nos 176 | están ayudando. Al menos, no todo lo que deberían. 177 | 178 | Lo que queremos comprobar en la prueba es, realmente, si hay una nueva tarea 179 | añadida. Una manera de probarlo es usar el método +pending_browns+. Uno podría 180 | pensar que no es una buena idea porque, si hay un error en +add_brown+ y otro 181 | en +pending_browns+ que se cancelen mutuamente, las pruebas pasarán igualmente. 182 | Eso es verdad, pero en la mayoría de los casos _no importa_, porque desde el 183 | punto de vista del usuario de la clase, ésta se comporta como debería. Cuando 184 | descubramos el fallo, lo podremos arreglar no sólo sin tener que cambiar las 185 | pruebas o el código que llama a +BrownList+, sino sin que haya habido ningún 186 | cambio en el comportamiento de +BrownList+ desde el punto de vista de los 187 | usuarios. 188 | 189 | [source,ruby] 190 | .Mejor ejemplo de prueba del método +add_brown+ de +BrownList+ 191 | ---------------------------------- 192 | class TestBrownList < Test::Unit::TestCase 193 | def setup 194 | # Recreamos la base de datos de pruebas para que esté vacía 195 | end 196 | 197 | def test_add_brown_simple 198 | db_handle = ... 199 | bl = BrownList.new(db_handle) 200 | bl.add_brown("Tarea de prueba") 201 | 202 | assert_equal 1, bl.pending_browns.length 203 | end 204 | end 205 | ---------------------------------- 206 | 207 | Para terminar de ilustrar este consejo, imaginemos ahora que escribimos una 208 | interfaz web para nuestra aplicación de tareas pendientes. Si queremos 209 | comprobar que la interfaz web funciona correctamente, una (mala) idea que puede 210 | pasarnos por la cabeza es comparar el HTML de la página con el HTML que 211 | esperamos. Si comparamos el HTML completo (o una captura de pantalla), 212 | nuestras pruebas serán muy, muy frágiles. Por ejemplo, nuestras pruebas 213 | fallarán cuando hagamos cualquiera de estos cambios: 214 | 215 | * Cambiar el id de algún elemento o el nombre de alguna clase CSS 216 | * Cambiar un elemento de sitio o intercambiar la posición de dos opciones en un 217 | menú 218 | * Añadir una nueva opción o información extra 219 | * Corregir una falta de ortografía o redactar un texto de forma diferente 220 | 221 | Si nuestras pruebas comparan la salida HTML exacta, implícitamente estamos 222 | definiendo nuestra aplicación no como una aplicación web con ciertas 223 | características, sino como una aplicación que genera ciertas cadenas de HTML. 224 | Ya que al usuario no le importa el HTML generado, sino que la aplicación 225 | funcione, podemos ver que este enfoque no es el más apropiado. 226 | 227 | Una forma mucho mejor de probar una aplicación web es _buscar_ las partes 228 | interesantes. Por ejemplo, comprobar que el título de la nueva tarea aparece en 229 | el contenido de la página justo después de crearla. O comprobar que ya no está 230 | ahí después de borrarla. O comprobar que, al renombrar una tarea, el título 231 | antiguo ya no aparece, pero sí el nuevo. Sin embargo, hacer esas comprobaciones 232 | directamente puede ser tedioso y puede añadir algo de fragilidad a nuestras 233 | pruebas, por lo que lo mejor es desacoplar los detalles del HTML generado de 234 | las comprobaciones que queremos hacer. Una de las técnicas para conseguir esto 235 | se conoce como _PageObjects_, pero explorar _PageObjects_ va mucho más allá del 236 | objetivo de este artículo. 237 | 238 | Como resumen de este consejo, podemos decir que las _pruebas no sólo deben 239 | fallar cuando hay algún problema, sino que también deben pasar mientras no haya 240 | ninguno_. 241 | 242 | 243 | No ejecutarlas con frecuencia 244 | ----------------------------- 245 | Las pruebas no son un añadido al código, son parte integrante de éste. 246 | Asimismo, ejecutarlas es parte del ciclo normal de desarrollo. Si no las 247 | ejecutamos con frecuencia, no van a ser tan efectivas. Primero, porque cuando 248 | haya fallos, es probable que sea más de uno. En ese caso, será más difícil 249 | encontrar el origen de éstos. ¿Es un solo error el que provoca todos los fallos 250 | en las pruebas, uno por cada prueba? Segundo, porque si hemos hecho muchos 251 | cambios desde la última vez que ejecutamos las pruebas, tendremos más código 252 | que revisar en busca del problema. 253 | 254 | Ejecutar las pruebas con frecuencia (idealmente, después de cada cambio que 255 | hacemos) hace que sea muy fácil encontrar la causa del error, porque lo único 256 | que puede haber sido la causa de los fallos son los cambios desde la última vez 257 | que las ejecutamos. Si ejecutamos las pruebas antes de mandar nuestros cambios 258 | al control de versiones, y vemos que una de las pruebas falla, será suficiente 259 | ejecutar +git diff+ (o +svn diff+ o similar) para ver qué cambios deben de 260 | haber producido el problema. Además, cuanto más alta sea la frecuencia con la 261 | que ejecutemos las pruebas, más seguros estaremos de que el código funciona 262 | correctamente. En la medida de lo posible, en el mundo de la programación es 263 | mejor evitar la fé: trabajaremos más tranquilos y con más confianza si podemos 264 | demostrar que el código funciona en los casos cubiertos por las pruebas. 265 | 266 | El último punto importante de este consejo es tener una máquina «neutral» que 267 | ejecute las pruebas automáticas que tengamos, _cada vez que alguien manda un 268 | cambio al control de versiones_. Las ventajas son muchas: 269 | 270 | * Incluso si alguien se olvida de ejecutar las pruebas antes de enviar los 271 | cambios, tenemos garantizado que las pruebas se ejecutarán. 272 | * Si alguien se olvida de añadir un fichero al control de versiones, ese 273 | fichero no aparecerá en la máquina de integración continua, por lo que las 274 | pruebas fallarán y nos daremos cuenta del error. 275 | * Los resultados de las pruebas en la máquina de integración continua son más 276 | fiables, porque tiene la misma configuración que las máquinas de producción. 277 | Por ejemplo, si un programador escribe una nueva prueba que depende de un 278 | nuevo módulo o de un cambio de configuración que sólo existe en la máquina de 279 | ese programador, la prueba pasará en su máquina, pero fallará en integración 280 | continua. Este fallo nos avisará del problema antes de que el proyecto pase a 281 | producción. 282 | * Como tenemos los resultados de las pruebas para cada cambio que se haya 283 | hecho, y ejecutados en la misma máquina, podemos saber qué cambio exacto 284 | produjo el problema, lo que hace mucho más fácil arreglarlo. 285 | 286 | Véase el artículo de Martin Fowler <<fowlerci>> sobre integración continua para 287 | más información. 288 | 289 | 290 | No controlar el entorno 291 | ----------------------- 292 | Otro problema bastante común es escribir pruebas sin controlar el entorno en el 293 | que se ejecutan. En parte esta (mala) costumbre viene de la creencia de que las 294 | pruebas tienen que adaptarse a diferentes circunstancias y ser robustas como 295 | los programas que escribimos. Esto es un malentendido. 296 | 297 | Volvamos al ejemplo anterior de la aplicación de tareas pendientes. Cuando 298 | escribimos las pruebas, los pasos _no_ fueron: 299 | 300 | 1. Obtener el número de tareas actuales, llamarlo _n_ 301 | 2. Añadir una tarea 302 | 3. Comprobar que el número de tareas actuales es _n_ + 1 303 | 304 | Los pasos fueron: 305 | 306 | 1. Dejar la base de datos en un estado conocido (en este caso, vacía) 307 | 2. Añadir una tarea 308 | 3. Comprobar que el número de tareas es _exactamente_ 1 309 | 310 | Esta diferencia es fundamental. Uno podría pensar que la primera prueba es 311 | mejor porque «funciona en más casos». Sin embargo, esto es un error por las 312 | siguientes razones: 313 | 314 | * Escribir código robusto requiere mucho más esfuerzo mental, especialmente a 315 | medida que crecen las posibilidades. Como no necesitamos esa robustez, mejor 316 | dejarla de lado. 317 | * Las pruebas serán menos flexibles, porque no podremos probar qué ocurre en 318 | casos específicos (p.ej. cuando hay exactamente 20 tareas, cuando hay más de 319 | 100 tareas, etc.). 320 | * Si no controlamos y rehacemos el entorno de ejecución de las pruebas, unas 321 | pruebas dependerán, potencialmente, de otras. Lo que significa que el 322 | comportamiento de unas pruebas puede cambiar el resultado de otras. En el 323 | caso ideal, que es el caso común, las pruebas se pueden ejecutar una a una 324 | independientemente y tienen exactamente el mismo resultado. 325 | * No siempre darán el mismo resultado, incluso cuando las ejecutemos por sí 326 | solas. Por ejemplo, digamos que hay un fallo en +add_brown+ que sólo aparece 327 | cuando hay más de 20 tareas. En ese caso, si nunca borramos la base de datos, 328 | nuestras pruebas fallarán... cuando las hayamos ejecutado suficientes veces. 329 | Y si las dejamos así, y hay otro fallo que sólo aparece cuando no haya 330 | ninguna tarea, las pruebas nunca nos avisarán del segundo fallo. 331 | 332 | Si queremos probar ciertos casos de datos iniciales, es más claro y más fiable 333 | probar esos casos expresamente y por separado. Tendremos la ventaja de que 334 | estará claro al leer las pruebas qué casos cubrimos, y ejecutar las pruebas 335 | _una sola vez_ nos hará estar seguros de que _todos_ los casos que nos 336 | interesan funcionan perfectamente. Como regla general, cualquier incertidumbre 337 | o indeterminismo sobre la ejecución o resultados de las pruebas que podamos 338 | eliminar, debe ser eliminado. 339 | 340 | Podemos terminar este consejo con una reflexión: _las pruebas no son mejores 341 | porque pasen con más frecuencia, sino porque demuestren que un mayor 342 | número de casos interesantes funcionan exactamente como queremos_. 343 | 344 | 345 | Reusar datos de prueba 346 | ---------------------- 347 | Cuando empezamos a escribir pruebas, algo que necesitamos con frecuencia son 348 | datos iniciales o de prueba (en inglés, _fixtures_). Si no tenemos una forma 349 | fácil de crear esos bancos de datos para cada prueba, tendremos la tentación de 350 | tener un solo conjunto de datos iniciales que usaremos en _todas_ las pruebas 351 | de nuestro proyecto. Aunque en algunos casos pueda resultar práctico compartir 352 | datos de prueba entre _algunas_ pruebas, esta costumbre puede traer un problema 353 | añadido. 354 | 355 | A medida que escribimos nuevas pruebas, éstas necesitarán más datos de 356 | contraste. Si añadimos estos datos a nuestro único conjunto de datos iniciales, 357 | cabe la posibilidad de que algunas de las pruebas antiguas empiece a fallar 358 | (p.ej. una prueba que cuente el número de tareas en el sistema). Si ante este 359 | problema reescribimos la prueba antigua para que pase con el nuevo conjunto de 360 | datos, estaremos haciendo más complejas nuestras pruebas, y además corremos el 361 | riesgo de cometer un fallo al reescribir la prueba antigua. Por no mencionar 362 | que si seguimos por este camino, puede que en la siguiente ocasión tengamos que 363 | reescribir dos pruebas. O cinco. O veinte. 364 | 365 | Todo esto está relacionado, en cierta manera, con el problema descrito en el 366 | anterior apartado: pensar en las pruebas como pensamos en el resto del código. 367 | En este caso, pensar que tener más datos de prueba es mejor, porque se parecerá 368 | más al caso real en el que se ejecutará el programa. Sin embargo, en la mayoría 369 | de los casos esto no representa ninguna ventaja, pero sí que tiene al menos una 370 | desventaja: cuando alguna prueba falle y tengamos que investigar por qué, 371 | será más difícil encontrar el problema real cuantos más datos haya. Si podemos 372 | escribir nuestras pruebas teniendo un solo objeto de prueba, o incluso ninguno, 373 | mejor que mejor. 374 | 375 | 376 | No facilitar el proceso de pruebas 377 | ---------------------------------- 378 | El apartado sobre ejecutar las pruebas con frecuencia ya mencionaba que las 379 | pruebas son parte integrante del código. Aunque no funciona exactamente de la 380 | misma manera ni tienen las mismas propiedades, sí que se tienen que mantener 381 | con el mismo cuidado y esfuerzo con el que mantenemos el resto del código. 382 | 383 | Este apartado hace hincapié en que tenemos que hacer lo posible para facilitar 384 | la escritura de pruebas. Éstas no son una necesidad molesta a la que tenemos 385 | que dedicar el menor tiempo posible: como parte integrante de nuestro código se 386 | merece la misma dedicación que el resto. Así, nuestro código de prueba debe ser 387 | legible, conciso y fácil de escribir. Si cuesta escribir pruebas, ya sea en 388 | tiempo, esfuerzo mental o líneas de código, tenemos un problema que debemos 389 | resolver, ya sea reorganizando código, escribiendo métodos de conveniencia o 390 | usando cualquier otra técnica que nos ayude. Desgraciadamente, muchos 391 | desarrolladores piensan que es normal que sea costoso escribir pruebas y no 392 | hacen nada por mejorar la situación. En última instancia, esto hace que el 393 | equipo escriba menos pruebas, y de peor calidad. 394 | 395 | Veamos un caso concreto. Digamos que queremos probar la interfaz web de nuestra 396 | aplicación de tareas pendientes. Una de las primeras pruebas que escribiríamos 397 | aseguraría que crear una tarea simple funciona. Una primera implementación 398 | podría quedar así: 399 | 400 | [source,python] 401 | .Ejemplo de prueba funcional difícil de escribir 402 | ---------------------------------- 403 | class TestBrownListDashboard_______MAL(unittest.TestCase): 404 | 405 | def setUp(self): 406 | # Rehacemos la base de datos y creamos el navegador en self.driver 407 | 408 | def testAddBrownSimple______MAL(self): 409 | self.driver.get("/") 410 | self.driver.findElementById("username").send_keys("usuario") 411 | self.driver.findElementById("password").send_keys("contraseña") 412 | self.driver.findElementById("login").click() 413 | 414 | new_brown_title = "My title" 415 | self.driver.findElementById("new_brown").send_keys(new_brown_title) 416 | self.driver.findElementById("create_brown").click() 417 | title_tag = self.driver.findElementByTagName("task-1-title") 418 | self.assertEqual(title_tag.text, new_brown_title) 419 | ---------------------------------- 420 | 421 | Aunque aisladamente, este código es relativamente fácil de leer y entender, tiene varios problemas: 422 | 423 | * No es todo lo compacto que podría ser 424 | * Contiene código que sabemos que se duplicará en otras pruebas 425 | * No contiene abstracciones, por lo que cuando haya cambios en la aplicación 426 | (digamos, el id de "username" o "password" cambia), tendremos que buscar 427 | dónde nos referimos a éstos para actualizar el código 428 | * No está escrito usando el lenguaje del dominio de la aplicación, sino usando 429 | el lenguaje de automatización de un navegador, por lo que es más difícil de 430 | leer y mantener 431 | 432 | Una alternativa mucho mejor sería la siguiente: 433 | 434 | [source,python] 435 | .Ejemplo de prueba funcional más fácil de escribir 436 | ---------------------------------- 437 | class TestBrownListDashboard(BrownFunctionalTestCase): 438 | 439 | def testAddBrownSimple(self): 440 | self.assertLogin("usuario", "contraseña") 441 | 442 | new_brown_title = "My title" 443 | self.createBrown(new_brown_title) 444 | self.assertBrownExists(new_brown_title) 445 | ---------------------------------- 446 | 447 | Las mejoras de la segunda versión son las siguientes: 448 | 449 | * +TestBrownListDashboard+ ahora hereda de una nueva clase, 450 | +BrownFunctionalTestCase+, que será una clase base para todas nuestras clases 451 | de prueba. Aquí añadiremos todo el código común a diferentes pruebas de 452 | nuestra aplicación. 453 | * Como tenemos una clase base, ya no necesitamos escribir el método +setUp+ 454 | porque ésta ya crea la base de datos e inicializa el navegador de prueba por 455 | nosotros. 456 | * Para abrir sesión, simplemente llamamos a un nuevo método +assertLogin+. No 457 | sólo es mucho más compacto y legible, sino que si alguna vez cambian los 458 | detalles de cómo iniciamos sesión, podemos simplemente cambiar la 459 | implementación de este método. 460 | * Crear una nueva tarea es tan fácil como llamar a un nuevo método 461 | +createBrown+, y comprobar que se ha creado correctamente se lleva a cabo llamando al método 462 | +assertBrownExists+. Dependiendo del caso, podríamos incluso haber creado un 463 | método +assertCreateBrown+, pero por ahora parece mejor dejar ambas 464 | operaciones separadas. 465 | 466 | Como se puede ver, una simple reorganización del código (del mismo tipo que 467 | haríamos con el código principal del programa) puede tener un impacto muy 468 | grande en la facilidad de mantenimiento de nuestras pruebas. 469 | 470 | La necesidad de facilitar la escritura de pruebas se extiende a todas las 471 | tareas relacionadas con probar nuestro código, no sólo mantener el código de 472 | pruebas automáticas. Digamos que escribimos un programa cliente-servidor. Si 473 | cada vez que encontramos un problema no somos capaces de depurarlo, o de 474 | asegurar si está de verdad arreglado o no porque no tenemos una forma fácil de 475 | probar el cliente o el servidor por separado, tenemos un problema. Una de las 476 | varias soluciones posibles es tener un cliente de prueba con el que podamos 477 | enviar al servidor cualquier petición que se nos ocurra, y un servidor de 478 | prueba con el que podamos enviar al cliente cualquier respuesta que se nos 479 | ocurra. Herramientas para capturar _fácilmente_ el tráfico entre cliente y 480 | servidor también pueden ahorrarnos mucho tiempo a la larga. 481 | 482 | Al fin y al cabo, estamos hablando de la calidad de nuestro trabajo, no en un 483 | sentido abstracto o teórico, sino en el sentido más pragmático desde el punto 484 | de vista del usuario. Si no podemos comprobar que nuestro programa se comporta 485 | debidamente, nos quedarán muchos fallos por descubrir, y por tanto que 486 | arreglar, que llegarán a los usuarios finales. 487 | 488 | 489 | Depender de muchos servicios externos 490 | ------------------------------------- 491 | El último consejo es el más avanzado, y es el consejo con el que hay que tener 492 | más cuidado al aplicarlo. La tendencia natural al crear entornos de prueba es 493 | replicar algo lo más parecido posible al entorno real, usando las mismas bases 494 | de datos, los mismos servidores y la misma configuración. Aunque esto tiene 495 | sentido y es _necesario_ en pruebas de aceptación y pruebas de integración 496 | footnote:[Las pruebas de integración son las más completas que hacemos, que 497 | determinan si el proyecto, como un todo, funciona desde el punto de vista del 498 | usuario.], puede ser bastante contraproducente en pruebas unitarias y 499 | similares, en las que sólo queremos probar componentes relativamente pequeños. 500 | 501 | Depender de servicios externos como una base de datos, un servidor web, una 502 | cola de tareas, etc. hace que las pruebas sean más frágiles, porque aumentan 503 | las posibilidades de que fallen por una mala configuración, en vez de porque 504 | hemos encontrado un problema en el código. Como en la mayoría de los tipos de 505 | pruebas, como las unitarias, sólo queremos probar que cierto componente 506 | concreto funciona correctamente, no hace falta que lo integremos con el resto 507 | de los componentes. En muchos casos, podemos reemplazar esos componentes con 508 | versiones «de pega» que se comporten como nos haga falta para cada prueba. 509 | 510 | Un ejemplo claro de las ventajas de usar componentes «de pega» es el desarrollo 511 | y prueba de una aplicación que usa una API, tanto en el caso de que escribimos 512 | sólo el cliente como en el caso de que escribimos tanto el cliente como el 513 | servidor. Aunque para hacer pruebas de integración deberíamos usar el servidor 514 | real, la mayoría de las pruebas tendrán un ámbito más limitado (bien sean 515 | pruebas unitarias, o pruebas funcionales que sólo cubran el comportamiento del 516 | cliente). Para éstas sabemos, al tener la documentación de dicha API: 517 | 518 | 1. Qué llamadas debe generar nuestra aplicación en ciertas situaciones. 519 | 520 | 2. Cómo tiene que reaccionar nuestra aplicación ante ciertas respuestas del 521 | servidor. 522 | 523 | Armados con este conocimiento, podemos diseñar pruebas que no dependen del 524 | servidor. No depender del servidor tiene varias ventajas, entre otras: 525 | 526 | * Las pruebas se ejecutan más rápido. 527 | 528 | * Las pruebas se pueden ejecutar sin tener conexión a internet o acceso al 529 | servidor (incluyendo que el servidor esté funcionando correctamente). 530 | 531 | * Cuando hay fallos, éstos son más fáciles de depurar y corregir. 532 | 533 | * Podemos probar muchas situaciones que no podemos reproducir fácilmente en un 534 | servidor real, como no poder conectar al servidor, que el servidor devuelva 535 | cualquier tipo de error, que devuelva ciertas combinaciones de datos que son 536 | difíciles de obtener, condiciones de carrera, etc. 537 | 538 | No todo son ventajas, claro. Si el servidor introduce cambios que rompen la 539 | compatibilidad con la documentación que tenemos, esos fallos no se descubrirán 540 | hasta las pruebas de integración. De manera similar, si nuestras pruebas 541 | dependen de un comportamiento concreto, documentado o no, y este comportamiento 542 | cambia, de nuevo no detectaremos estos fallos hasta las pruebas de integración. 543 | 544 | Veamos un ejemplo más concreto. Digamos que escribimos un programa que utiliza 545 | la API pública de Kiva, una organización que permite, mediante microcréditos, 546 | prestar dinero a personas que lo necesitan. Nuestra aplicación mostrará los 547 | últimos préstamos listados en la web de Kiva (digamos, 50), información que 548 | obtenemos usando la llamada +/loans/newest+. Sin embargo, hay varios casos que 549 | son muy difíciles de probar con un servidor real: 550 | 551 | * Esa funcionalidad de la API sólo devuelve 20 elementos por llamada, por lo 552 | que tenemos que hacer varias llamadas para obtener todos los préstamos que 553 | queremos. 554 | * Si se añaden nuevos préstamos mientras estamos haciendo las llamadas para 555 | obtener 50 préstamos, tendremos préstamos repetidos, lo que queremos evitar. 556 | * Puede ocurrir que no haya 50 préstamos en un momento dado, así que tendremos 557 | que conformarnos con los datos que haya (en vez de, por ejemplo, entrar en un 558 | bucle infinito). 559 | * Puede que el servidor tenga algún problema y no devuelva una respuesta 560 | correcta, o que simplemente haya problemas de conectividad entre el cliente y 561 | el servidor. Como mínimo queremos asegurarnos, como en el caso anterior, de 562 | que el cliente no se queda en un bucle infinito, haciendo peticiones 563 | ciegamente hasta que hayamos recibido 50 préstamos. 564 | 565 | Probar todos esos casos a mano con el servidor real de Kiva es prácticamente 566 | imposible, principalmente porque no podemos hacer que el servidor devuelva las 567 | respuestas necesarias para reproducir cada caso. Si todas nuestras pruebas 568 | dependen del servidor no podremos estar seguros de si el código funciona bien. 569 | Sin embargo, todos estos casos son muy fáciles de probar si evitamos 570 | conectarnos al servidor real. Los casos mencionados arriba se podrían escribir 571 | de la siguiente manera en Javascript, usando Jasmine: 572 | 573 | [source,javascript] 574 | .Ejemplo de cómo probar el cliente ficticio de la API de Kiva 575 | ---------------------------------- 576 | it("should correctly get items from several pages", function() { 577 | var fakeServer = new FakeServer(fixtureWith100Loans); 578 | var kivaLoanLoader = new KivaLoanLoader(fakeServer); 579 | kivaLoanLoader.fetchLoans(50); 580 | expect(kivaLoanLoader.loans.length).toEqual(50); 581 | expect(kivaLoanLoader.loans[0].id).toEqual("loan1"); 582 | expect(kivaLoanLoader.loans[50].id).toEqual("loan50"); 583 | }); 584 | 585 | it("should correctly skip items duplicated in different pages", function() { 586 | var fakeServer = new FakeServer(fixtureWith100LoansSomeRepeated); 587 | var kivaLoanLoader = new KivaLoanLoader(fakeServer); 588 | kivaLoanLoader.fetchLoans(25); 589 | expect(kivaLoanLoader.loans.length).toEqual(25); 590 | expect(kivaLoanLoader.loans[19].id).toEqual("loan20"); 591 | // El siguiente caso será loan20 repetido, si el código no funciona bien 592 | expect(kivaLoanLoader.loans[20].id).toEqual("loan21"); 593 | expect(kivaLoanLoader.loans[24].id).toEqual("loan25"); 594 | }); 595 | 596 | it("should stop when there's no more data", function() { 597 | var fakeServer = new FakeServer(fixtureWith30Loans); 598 | var kivaLoanLoader = new KivaLoanLoader(fakeServer); 599 | // La línea siguiente será un bucle infinito si el código no es correcto 600 | kivaLoanLoader.fetchLoans(40); 601 | expect(kivaLoanLoader.loans.length).toEqual(30); 602 | expect(kivaLoanLoader.loans[0].id).toEqual("loan1"); 603 | expect(kivaLoanLoader.loans[29].id).toEqual("loan30"); 604 | }); 605 | 606 | it("should stop on server errors", function() { 607 | var fakeServer = new FakeServer(fixtureWithOnlyServerError); 608 | var kivaLoanLoader = new KivaLoanLoader(fakeServer); 609 | // La línea siguiente será un bucle infinito si el código no es correcto 610 | kivaLoanLoader.fetchLoans(20); 611 | expect(kivaLoanLoader.loans.length).toEqual(0); 612 | }); 613 | ---------------------------------- 614 | 615 | 616 | 617 | Conclusión 618 | ---------- 619 | Probar programas es una tarea importante pero bastante difícil de hacer 620 | correctamente. Sin embargo, si empezamos a hacer pruebas desde el comienzo del 621 | proyecto, las ejecutamos con frecuencia y nos preocupamos de que sean fáciles 622 | de mantener, nuestras probabilidades de producir programas robustos serán mucho 623 | más altas. 624 | --------------------------------------------------------------------------------