quinta-feira, 17 de dezembro de 2009

Ver um Json no Browser Firefox de uma maneira decente.

Para ver um Json no firefox, use este plugin:

http://benhollis.net/blog/2009/02/24/jsonview-view-json-documents-in-firefox/

terça-feira, 6 de outubro de 2009

Tá difícil escrever testes para o seu código?

Claro que sim. Você escreveu o código primeiro. Que tal escrever código para os seus testes. Agora sim. Se precisar de refactoring, os testes garantem. Agora aquele código sem testes vai ter que ter refactoring para poder ser testado? Duas perdas de tempo:

1 refactoring sem uma malha de testes (tortura é bem parecido)
2 escrever testes tão depois de escrever código.

Além disso com certeza aí tem código inútil.... não foi escrito para passar nos testes, mas sim por que talvez um dia você possa precisar não é mesmo? O nome desse código para alguns pode ser "visão de futuro", mas para mim chama "teia de aranha". Só serve para se enrolar nele e atrapalhar um caminhar mais direto.

Digo isso vivi isso no dia a dia agorinha mesmo. Testes primeiro, caminho tranquilo, por a cabeça no travesseiro e dormir...

quinta-feira, 9 de julho de 2009

Gerenciando plugins em um repositório separado com svn:externals.

Se temos um código que queremos usar em dois projetos, mas por algum motivo, não queremos manter em um lugar centralizado e usar o "include_path" do php. Podemos criar um repositório só para este código e gerencia-lo como um novo projeto.

Este recurso é muito útil para plugins que são usados em mais de um projeto. Procurando na net é um tanto fraca a documentação a respeito de como usar o recurso svn:externals. No svn book ele mostra as informações fragmentadas e não o processo todo.

Neste texto eu descrevo o processo de criar um novo repositório, exportar os arquivos para ele e então gerar o link do externals apontando para o novo repositório. Vou descrever boa parte destas tarefas considerando o uso do tortoise no lado do cliente, mas a parte do externals também vou colocar a linha de comando, visto que a documentação no livro é muito fragmentada.

1 - Exportar o arquivos para uma nova pasta.

Usando o tortoise, escolhemos a pasta e selecionamos "export". Isso irá criar uma nova pasta não versionada, com tudo o que tinha ali dentro. Não versionada quer dizer sem aquele monte de .svn que ele cria.


2 - Criar o repositório no servidor.

svnadmin create jsCallPlugin

Neste ponto é importante verificar se a pasta tem permissões para ser alterada pelo usuário do svn. Existem várias maneiras de se fazer isso, mas uma que pode ser utilizada é atribuir ao diretório um grupo compartilhado por todos os usuários do svn.


3 - Fazer um check out do repositório na sua máquina.

com o tortoise, é só clicar na pasta e então escolher o checkout, colocar o endereço e pronto.


4 - Mandar os arquivos pro novo repositório.

Lembra aqueles arquivos que fizemos export. Pois é, estes arquivos devem ser copiados para o repositório. Veja que é uma boa prática criar as pastas trunk, branches e tags. Neste caso podemos copiar nossos arquivos para dentro da trunk. Depois é só fazer o "commit" normalmente.

Este é o método que considero mais simples, mas você também pode, tendo os arquivos no servidor, usar um import para coloca-los no novo repositório.


5 - Definir o externals.

Finalmente o que interessa.

O svn:externals é uma propriedade que atribuimos a um diretório na nossa cópia de trabalho. Esta propriedade será enviada para o servidor quando fizermos o commit. Esta propriedade indica que, ao se atualizar uma cópia de trabalho, o svn deverá buscar, para colocar dentro do diretório ao qual atribuimos a propriedade, dados de um outro repositório. Então precisamos entender 3 coisas nessa configuração:

a) o diretório que tem a propriedade (diretório local)
b) o caminho do repositório
c) o nome do novo diretório que será criado (diretório externo)

Sabendo destes elementos fica fácil não cair em erros comuns que vemos pelos foruns da net.

Então, para fazer o externals do jeito mais difícil (no dos) usamos o seguinte comando:

svn propedit svn:externals . --editor-cmd edit

O ponto é o endereço do diretório local ao qual atribuimos a prop. (que pode ser trocado, é claro)

O comando --editor-cmd indica que vamos usar o edit.bat para editar as propriedades.

bom, dentro deste arquivo precisamos colocar os nomes dos diretórios externos e seus respectivos repositórios:

dir1 http://repos/plugin1/trunk
dir2 http://repos/plugin2/trunk

e depois é só salvar o arquivo normalmente. No próximo update, o svn vai buscar destes repositórios e criar as pastas dir1 e dir2, trazendo o conteúdo de plugin1 e plugin2 respectivamente.

Bom, e do jeito mais fácil ?

No tortoise, basta clicar sobre o diretório local e escolher o comando properties (dentro dos comandos do tortoise, é claro). Com isso vai aparecer uma lista das propriedades e suas configurações. Adicione a propriedade svn:externals com uma linha com "[nomedodiretorio] [urldorepositorio]" para cada externals


sexta-feira, 12 de junho de 2009

Atribuindo métodos a tipos do js, adicionando um método .repeat() a todas as strings.


String.prototype.repeat = function(numTimes) {
var ret = '';
for(var a = 0; a < numTimes;a++) {
ret += this;
}
return ret;
}

quarta-feira, 13 de maio de 2009

Usando classes do Zend Framework no Symfony

1 - incluir as classes em plugins/Zend/lib

2 - configurar o caminho no settings.yml

# Zend Framework
zend_autoload: on
zend_lib_dir: "%SF_PLUGINS_DIR%/Zend/lib"

3 - inicializar o autoload no frontendConfiguration:

/**
* @package system
* @subpackage configuration
*/
class frontendConfiguration extends sfApplicationConfiguration
{
public function configure()
{

}

public function initialize()
{
parent::initialize();
/**
* Zend Framework Autoloading
*/
if(sfConfig::get('sf_zend_autoload', true) && ($sf_zend_lib_dir = sfConfig::get('sf_zend_lib_dir'))) {
set_include_path($sf_zend_lib_dir.PATH_SEPARATOR.get_include_path());
require_once($sf_zend_lib_dir.DIRECTORY_SEPARATOR.'Zend'.DIRECTORY_SEPARATOR .'Loader.php');
spl_autoload_register(array('Zend_Loader', 'loadClass'));
}

/**
* DOMPdf Autoloading
*/
if(sfConfig::get('sf_dompdf_autoload', true) && ($sf_dompdf_lib_dir = sfConfig::get('sf_dompdf_lib_dir'))) {
set_include_path($sf_dompdf_lib_dir.PATH_SEPARATOR.get_include_path());
// require_once($sf_dompdf_lib_dir.DIRECTORY_SEPARATOR.'dompdf_config.inc.php');
// spl_autoload_register('DOMPDF_autoload');
}
}
}

terça-feira, 14 de abril de 2009

Lendo um diretório com SPL



$r = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(dirname(__FILE__)),
RecursiveIteratorIterator::LEAVES_ONLY
);

foreach ($r as $file) {
echo $file.'
';
}

terça-feira, 7 de abril de 2009

Pequenos ajustes no mysql.... (bin/my.cnf)

Para configurar o log das querys:
log="C:/tmp/mysqllog/mysqld.log"

Para definir o charset padrão
character_set_server=UTF8

terça-feira, 31 de março de 2009

[snippet] Adicionando inputs ao pressionar enter

Inserindo mais inputs no enter:

adminEssays.form = {};

adminEssays.form.addInput = function(target) {
var nextInput;
inputs = $("#category-subs").children('input');
if(inputs.length == 0) {
nextInput = 0;
}
else {
nextInput = 0;
for (var a = 0; a < inputs.length; a++) {
id = parseInt(inputs[a].id.substr(21));
if (id >= nextInput) {
nextInput = id + 1;
}
}
}
id = "category-subcategory-" + nextInput;
html = "< input type='text' size='70' id='" + id + "' class='category-subcategory' name='subcategory[]' style='display:block'/>";
if(nextInput == 0) {
$('#category-subs').html(html);
}
else {
$(target).after(html);
}
$(".category-subcategory").bind("keypress",adminEssays.form.inputEnter);
}

adminEssays.form.submitForm = function(id) {
JsonC.submitForSfAction('adminEssays/insertSubcategories',{},'form-insert-subcategories','form-insert-subcategories-loading');
}

adminEssays.form.inputEnter = function(evt) {
if(evt.keyCode == 8 || evt.keyCode == 46) {
var tgt = $(evt.currentTarget);
if(tgt.val() == '') {
if( $('#category-subs').children().length > 1 ) {
var inputToFocus;
if(tgt.next().length > 0) {
inputToFocus = tgt.next()[0];
}
else {
inputToFocus = tgt.prev()[0];
}
tgt.remove();
inputToFocus.focus();
}
return false;
}
}
if(evt.keyCode == 13) {
prox = $(evt.currentTarget).next().length;
if(prox > 0) {
$($(evt.currentTarget).next()[0]).focus();
}
else {
adminEssays.form.addInput(evt.currentTarget);
$($(evt.currentTarget).next()[0]).focus();
}
}
};

quinta-feira, 5 de março de 2009

Criando uma estrutura de arvore para o jsTree


public function createTreeStructure($dir) {
$ar = array(
'attributes'=>array('id'=>'node-root'),
'data'=>'TESTS',
'children'=>$this->createChildren($dir)
);
return $ar;
}

public function createChildren($dir) {
static $n=0;
$it = new RecursiveDirectoryIterator($dir);
$arAll = array();
foreach($it as $file) {
if(strpos($file,'.svn') !== false) continue;
$ar = array(
'attributes'=>array('id'=>'node-'.$n),
'data'=>$file->getFilename()
);
if($file->isDir()) {
$ar['children'] = $this->createChildren($file);
}
$arAll[] = $ar;
}
return $arAll;
}

O valor de longo prazo de um software é diretamente proporcional a qualidade do código.

Já imaginou se todo gerente e toda organização tivessem esta consciência? Seria bem mais fácil de fazer um bom trabalho. Pena que de maneira geral o brasileiro ainda não tem visão de médio e longo prazo.

O que acontece é que o custo de desenvolvimento de um software, se for bem feito, pode reduzir muito o custo do próximo pelas bibliotecas geradas. E também a manutenção em bom código é muito mais fácil.

segunda-feira, 2 de março de 2009

Module Pattern

Existem diversos bons artigos na net a este respeito. Aqui não vou tentar explorar todas as possibilidades deste padrão. Pretendo apenas dar uma explicação básica do seu funcionamento.

O "Module Patern" tira proveito dos "closures" do JS. Quando uma função declara variáveis dentro dela e também declara outras funções ou objetos estes objetos e funções têm acesso às variáveis declaradas. Ex:


pessoa = function(nome,telefone) {
var nome = nome;
var telefone = telefone;
this.nome2 = "nome2 - " + nome;
return {
mostra: function() {
document.writeln('Nome:' + nome + '< br / >');
document.writeln('Telefone:' + telefone + '< br / >');
}
};
}


A função "mostra" do objeto retornado no código acima, por estar definida dentro da função pessoa, tem acesso às variáveis "nome" e "telefone".

O interessante é que mesmo depois de a função pessoa ter sido executada, o objeto continua tendo acesso àquelas variáveis. Isto quer dizer que se chamarmos:


p = pessoa('joao','3344 5678');


vai acontecer o seguinte:

1 - a função pessoa será executada declarando as variáveis "nome" e "telefone"
2 - ela vai retornar um objeto, definido por um código JSON, com um método mostra()
3 - este método mostra vai ter acesso às variáveis "nome" e "telefone" já declaradas.
4 - como a função já foi executada, não temos mais como mexer nas variáveis "nome" e "telefone" a não ser pelo "mostra".
5 - com isso criamos um escopo "privado" para as variaveis definidas.

O mesmo método pode ser usado para retornar uma função:


mostraLista = function(nome,telefone) {
var lista = {'segredo1':'noite','segredo2':'dia'};
return function() {
for(i in lista) {
document.writeln(i + ':' + lista[i] + '< br / >');
}
}
}();


Neste caso repare que depois da declaração já executamos a função. Com isso fazemos o seguinte:

1 - a função é executada declarando o objeto "lista";
2 - ela retorna uma outra função, que fica atribuida a "mostraLista"
3 - esta função retornada fica com acesso "privado" a variável "lista"

assim podemos chamar


mostraLista();


para que este método seja executado com acesso ao objeto. Porém não podemos acessar diretamente por "mostraLista.lista" pois este objeto não existe, o que existe é o acesso dele à variável da função que já foi executada. Caso você queira experimentar, veja que o valor será igual a "undefined".

terça-feira, 17 de fevereiro de 2009

Chamada a métodos no Js.

Este post é do tipo "um código vale mais do que mil palavras", assim sendo dificilmente você vai aprender alguma coisa apenas vendo o código escrito. Porém se você pegar cada pequeno bloco de código e rodar (O que pode ser feito, acredito, até com o firebug), você vai provavelmente entender os tipos possíveis de chamada em js e o que acontece com a palavra-chave "this" em cada um dos tipos. Podemos entender isso como qual o escopo no qual a função é rodada.

Vale uma ressalva de que na maioria, o conteúdo que escrevo aqui, aprendi no excelente livro que recomendo: Javascript: The Good Parts - O'REILLY, Douglas Crockford.

Bão, ao código...

O Js é uma linguagem atípica, herança por "prototype", sem classes, funções são objetos, vinculação tardia da palavra chave "this", etc... Por conta disso muita gente (eu inclusive) que começa a trabalhar com ele não entende nada. Assim vejo que é importante esclarecer aspectos "simples" da linguagem, como o que acontece quando chamamos uma função.

Aqui vão exemplos de quatro tipos de chamada:

Chamada á Função

// quando uma função não pertence a nenhum método...
function ola() {
document.writeln('ola
');
// o this neste caso é vinculado ao objeto global.
document.writeln(this);
}
ola();

####### com funções internas
/*
já no caso da função estar dentro de uma outra é necessário
uma "solução de contorno" para ter acesso às propriedades da externa.
*/
var ola2 = function() {
var that = this;
this.texto = "ola";
var fala = function() {
document.writeln(that.texto + "2
");
// segundo o livro, se usasse this direto aqui dentro não
// funcionaria pois ele ficaria vinculado ao objeto global.
// mas no exemplo abaixo, funciona.
document.writeln(this.texto + "---2
");
this.texto2 = 'aaaaaa';
}
fala();
document.writeln('texto2:' + this.texto2 + "
");
}
//ola2.fala(); -> daria erro pois não foi ligado ao this de ola2, é apenas funçao interna.
ola2();


Chamada ao Construtor
* esta maneira é indicada como a "pior dos dois mundos", pois imita uma linguagem com classes, mas não tem, e não deixa claro o uso do "prototype".

var Pessoa = function(nome) {
this.nome = nome;
}
/*
chamada ao construtor:
um novo objeto é criado com um link escondido para o valor do prototype da função
*/
p = new Pessoa("Fulano");
// veja que o método foi adicionado depois do objeto ser criado
Pessoa.prototype.escreveNome = function() {
document.writeln('+++++'+this.nome+'+++++
');
}
// procura em p, não tem, busca no Pessoa.prototype...
p.escreveNome();

p.escreveNome = function() {
document.writeln('-----'+this.nome+'-----
');
}

// procura em p, tem, usa o dele mesmo
p.escreveNome();

// remove o método do objeto p
delete(p.escreveNome);

// procura em p novamente, não tem pois foi apagado, usa novamente o do prototype
p.escreveNome();



Chamada ao Método

var o = {
linha:"",
aumentaLinha: function() {
this.linha += "---";
},
mostraLinha: function() {
document.writeln(this.linha + '0' + this.linha + '
');
}
}

// a vinculação tardia do "this" ao objeto ocorre apenas na chamada ao método.
for(var a = 0; a<30; a++) {
// duas maneiras de se chamar o mesmo método:
o.mostraLinha();
o['aumentaLinha']();
}

Chamada Com Apply

// apply é um método presente em todas funções de js.
// lembrando que as funções de js são objetos e podem ter métodos.
mostraNome = function(extra) {
document.writeln("this.nome: " + this.nome + "
");
document.writeln("param extra: " + extra + "
");
}
mostraNome();
document.writeln("valor de mostraNome.apply:" + mostraNome.apply + "
");
document.writeln("
");
/*
apply chamará o método mostraNome, vinculando o this ao objeto passado
no primeiro parâmetro. E e ele vai passar para o mostra nome o array
de parâmetros passado no segundo parâmetro.
*/
mostraNome.apply({nome:"João "},["pereira dos santos"])
//

sexta-feira, 13 de fevereiro de 2009

Chamando o callback do callback no jquery.

Me deparei com um problema ao precisar chamar o callback de vários nós de uma arvore para abrir um de cada vez no jquery. Ou seja, tem que abrir a raiz e passar o callback para abrir o filho, só que no callback de abrir o filho (callback do callback) chamar o callback de abrir o neto e assim por diante até onde for necessário.

Depois de fritar os miolos um poucos, resolvi da seguinte maneira:
Na "classe" TaskTreeC coloquei um método que inicializa algumas variáveis no escopo dela mesma e também define um lock:

  
this.openParents = function(id,callback) {
// para não dar conflito sendo chamada antes de terminar uma operação em andamento.
if(TaskTreeC.locked) return;
TaskTreeC.locked = true;
stack = [];
TaskTreeC.getParentStack(id,stack);
TaskTreeC.stack = stack;
TaskTreeC.recursiveIndex = 0;
TaskTreeC.openStackCallback = function() {
TaskTreeC.locked = false;
if(callback) callback();
}
TaskTreeC.openStack();
}


Primeiramente a função recebe um id de um item interno na arvore e chama getParentStack para poder pegar um array com os ids de todos os pais daquele item. Então armazena isso, cria um índice e um callback final que será chamado depois de todo o processo. Então ela chama o método openStack que faz o "serviço sujo".

No meu caso eu não preciso mostrar os elementos percorridos, mas sim abrir os filhos de cada um deles. Assim o método openStack ficou da seguinte maneira:


this.openStack = function() {
if(TaskTreeC.recursiveIndex < TaskTreeC.stack.length) {
id = TaskTreeC.stack[TaskTreeC.recursiveIndex];
TaskTreeC.recursiveIndex++;
if(TaskTreeC.getChildren(id).length) {
// pega o div que tem os filhos...
node = $('#node-content-'+id);
if(node.is(':visible')) {
// se já estiver aberto, segue em frente sem fazer nada.
TaskTreeC.openStack();
}
else {
// mostra com um tempo para o browser renderizar
var t=setTimeout(
"TaskTreeC.removeHiddenCache(id);"+
"node.show('blind',{},500,TaskTreeC.openStack);"+
"TaskTreeC.closeChildren(id);", // fecha os "netos"
200
);
}
}
else {
// chama o callback final
if(TaskTreeC.openStackCallback) {
var t=setTimeout("TaskTreeC.openStackCallback();",300);
}
}
}
else {
// chama o callback final
if(TaskTreeC.openStackCallback) {
var t=setTimeout("TaskTreeC.openStackCallback();",300);
}
}
}



Outros métodos usados:



this.getParentStack = function(id,stack) {
node = $('#node-'+id);
parentId = UtilC.getAttrUpTheTree( node.parent() , 'taskId' );
if(parentId) {
TaskTreeC.getParentStack(parentId,stack);
}
stack[stack.length] = id;
}

//UtilC
this.getAttrUpTheTree = function(item,attrName) {
//attrib = $(item).attr(attrName);
if(
$(item).attr(attrName) != undefined &&
( typeof $(item).attr(attrName) === 'string' )
) {
return $(item).attr(attrName);
}
else {
parentNode = $(item).parent()[0];
if(parentNode != undefined) {
return RouterC.getAttrUpTheTree($(item).parent()[0],attrName);
}
else return null;
}
}


OBS: acho que este código pode ser melhorado utilizando um module pattern, dê uma olhada no outro post.

quarta-feira, 11 de fevereiro de 2009

Dicas para escrita e uso de testes

Mantenha TODOS os testes passando e atualizados.

Toda vez antes de dar commit, faça um update e rode toda a suite de testes na sua máquina. Caso algum teste quebre, concerte o que precisa ser concertado antes de mandar para o repositório. Lembre-se que o código pertence a todos. Você pode consultar quem escreveu o código/teste, mas se tem um teste quebrado na sua máquina, é seu dever fazê-lo passar.

Deixar testes desatualizados no sistema pode gerar um efeito de bola de neve e mais na frente atrapalhar muito o desenvolvimento. Além disso manter testes quebrados no sistema certamente vai gerar mais erros e diminuir a confiança da equipe no processo de testes.

Escreva testes atômicos do ponto de vista do sistema.

Cada teste deve ser contido em si. Se o teste depende de algum estado do sistema, busque criar o cenário antes de rodar o teste. (ex: o teste do captcha para o login depende de ter a contagem limpa. Assim limpamos a contagem e rodamos o teste.) Também é uma boa prática limpar a casa depois de usar. Assim quando terminarmos de rodar o teste é importante excluir registros e informações que possam interferir em outros testes.

A classe de teste também deve ser o mais auto-suficiente possível. Não crie interdependências entre testes nem use métodos de outra classe te teste. Isso pode dar sérios problemas e dificuldades de manutenção.

Escreva testes atômicos do ponto de vista das funcionalidades.

A não ser se você estiver fazendo um teste de integração, não escreva testes que testam tudo de uma vez. Teste cada funcionalidade de uma vez. Isso deixa os testes fáceis de manter e os relatórios muito melhores. Lembre que a lista dos testes faz as vezes de uma especificação do negócio em muitos casos.

Por exemplo. Ao invés de escrever: "A listagem deve ser mostrada corretamente." Escreva algo como: "deve mostrar a soma dos valores no campo Valor Total", "deve paginar caso haja mais de 15 registros", etc...

Escreva testes rápidos

Busque manter os testes o mais rápido que for possível. Ao caminhar com o desenvolvimento teremos cada vez mais teste e não queremos que o build fique muito lento. Por isso precisamos economizar ao máximo em cada um dos testes.

No selenium evite expressões como "waitForText" ou "waitFor..." qualquer coisa que você não tenha certeza que não vai aparecer. Busque esperar por elementos certos na página e então dê um assert para ver se ele existe. Um bom elemento para usarmos em requisições de Ajax é o indicador de progresso.

Escreva testes que independam ao máximo da interface

Ao escrever um teste, busque testar elementos que você sabe que não vão ser alterados durante o processo de desenvolvimento. Por exemplo, ao invés de testar por uma mensagem, busque testar por elementos da página que provavelmente não vão mudar com o tempo. Ou então vc pode buscar pela constante que indica o valor da mensagem e não pela string.

Criando testes no PHPUnit/Selenium que rodem com o symfony/doctrine/phing/phpunit

Ingredientes

symfony+doctrine: framework de desenvolvimento e ORM
phpunit: para testes unitários
phing: para fazer o build, rodar os testes, gerar a documentação, lavar a roupa, etc...

Problemas a se Vencer:

O phpunit, quando roda pela linha de comando, não carrega as configurações do projeto do symfony.

Alguns testes do selenium precisam de acesso à api do symfony para configurar o ambiente e ter acesso aonde o browser não tem.

Descrição da Solução:

Para isso precisamos carregar as configurações do symfony/docrine. Claro que é bom fazer isto uma vez só para não pesar nos testes. Porém precisamos fazer isto de uma maneira que não prenda muito os testes a um diretório específico e que não seja repetitiva.

O phpunit, quando rodado pelo phing, não considera as suites de teste, rodando todos os testes separadamente. Por isso, o setUp() da suite, que poderia parecer um bom local, na verdade não é pois ele será desconsiderado pelo phing. Escolhemos então o setUp de cada classe teste. Para não repetir o código, criamos uma classe da qual extendemos todos os nossos testes. Assim nossas classes já herdam o setUp. Como o autoload do symfony ainda não foi carregado, precisamos informar para o phpunit o caminho da nossa classe caso ela não esteja no include.

Solução Proposta:


### classe pai

class Sf_PHPUnit_Extensions_SeleniumTestCase extends PHPUnit_Extensions_SeleniumTestCase {
function setUp()
{
if ( SF_APP_NAME != '' )
{
define( 'SF_APP_NAME', 'frontend' );
define( 'SF_ENV', 'test' );
define( 'SF_CONN', 'doctrine' );
// ajuste o caminho abaixo de acordo com o local onde vc por esta classe
require_once(dirname(__FILE__).'/../../../../config/ProjectConfiguration.class.php');
// busca as configurações do projeto
$configuration = ProjectConfiguration::getApplicationConfiguration( SF_APP_NAME , SF_ENV, true);
sfContext::createInstance($configuration);
sfContext::getInstance()->getUser()->initSession('phpunit');
// em alguns contextos as linhas abaixo podem ser úteis, mas não lembro quais agora :)
//$databaseManager = new sfDatabaseManager($configuration);
//$databaseManager->loadConfiguration();
}

$this->setBrowser("firefox");
// "solução de contorno" para definir se o teste está no ambiente de desenv ou homolog.
if(isset($_SERVER[HOMEDRIVE]) && $_SERVER[HOMEDRIVE] == 'C:') {
$this->setBrowserUrl("http://aliaslocal/");
}
else {
// ip ou alias do servidor...
$this->setBrowserUrl("http://10.1.1.234/");
}
}
}

### classe filha


require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
// lembrar que este require é necessário pois o autoload do symfony ainda não foi carregado neste ponto.
require_once dirname(__FILE__).'/../../../plugins/sfUtil/lib/test/SF_PHPUnit_Extensions_SeleniumTestCase.php';

class LoginSeleniumTest extends SF_PHPUnit_Extensions_SeleniumTestCase
{

function setUp() {
// lembrar de chamar o setUp do pai caso precise extender aqui...
parent::setUp();
$this->doSomeMoreSetup();
}
// ...

Conclusão:

Claro que devem existir inúmeras soluções para este problema. Caso você tenha algo a acrescentar ou alguma crítica construtiva, certamente isto vai melhorar ainda mais a nossa proposta.