December 30, 2014

Vim: Switch Between Tests (Or Other Files)

As most people I am annoyed with repeating stuff over and over. And today my workflow about switching between test files pissed me off. So I wrote two little functions that switches between unit tests and their corresponding SUTs. It also creates them if non-existent.

Function one: self-contained Symfony bundles

If you prefer to write self-contained bundles that also contain their tests, following will work.

Directory structure

projectroot/
- src/Your/Bundle/Service/
- src/Your/Bundle/Tests/Service

Vim function

nnoremap <leader>ta :call SymfonySwitchToAlternateFile()<cr>
nnoremap <leader>tsa <c-w>v:call SymfonySwitchToAlternateFile()<cr>

" changes to test/sut files
" following structure:
" sut:  src/Acme/Bundle/Service/MyService.php
" test: src/Acme/Bundle/Tests/Service/MyServiceTest.php

function! SymfonySwitchToAlternateFile()
    let l:f = expand('%')
    let l:preceedingDirsToKeep = 2
    let l:is_test = expand('%:t') =~ "Test\."
    if l:is_test
        " remove phpunit_testroot
        let l:f = substitute(l:f, 'Tests/','','')
        " remove 'Test.' from filename
        let l:f = substitute(l:f,'Test\.','.','')
    else
        let l:pathParts = split(expand('%:r'), '/')
        let l:startingPath = l:pathParts[0:l:preceedingDirsToKeep]
        let l:endPath = l:pathParts[(l:preceedingDirsToKeep+1):]
        let l:combinedPath = l:startingPath + ['Tests'] + l:endPath
        let l:f = join(l:combinedPath, '/') . 'Test.php'
        if !filereadable(l:f)
            let l:new_dir = substitute(l:f, '/\w\+\.php', '', '')
            exe ":!mkdir -p ".l:new_dir
        endif
    endif
    " is there already a window the wanted file?
    let win = bufwinnr(l:f)
    if l:win > 0
        execute l:win . "wincmd w"
    else
        execute ":e " . l:f
    endif
endfunction

Function two: identifier in project root

I’m currently working on a Symfony project that uses Codeception for its testing. Codeception puts all tests into a tests/<testtype> directory in your project root. <testtype> might be unit, functional or acceptance, i.e. ‘tests/unit/..’.

Directory structure

projectroot/
- src/Your/Bundle/...
- tests/unit/src/Your/Bundle/...
- tests/functional/src/Your/Bundle/...
- tests/acceptance/src/Your/Bundle/...

Vim function

nmap <leader>tu :call SwitchBetweenFiles('php', 'tests/unit/', 'src/', 'Test')<cr>
nmap <leader>tsu <c-w>v:call SwitchBetweenFiles('php', 'tests/unit/', 'src/', 'Test')<cr>
nmap <leader>tf :call SwitchBetweenFiles('php', 'tests/functional/', 'src/', 'Test')<cr>
nmap <leader>ta :call SwitchBetweenFiles('php', 'tests/acceptance/', 'src/', 'Test')<cr>

" Parameters:
"
" fileExtension: the file extension this script should act on, i.e. 'php'
" (without dot)
"
" firstDirBeginning: a file path that identifies the first type
" of paths, i.e. 'tests/unit/'
"
" secondDirBeginning: a file path pattern that identifies the second type
" of paths, i.e. 'src/'
"
" filenameAddition: string that should be removed from the first filename and
" added to the second. Should be for example 'Test' if your testfile filename has the suffix
" 'Test' as in /path/MyServiceTest.php

function! SwitchBetweenFiles(fileExtension, firstDirBeginning, secondDirBeginning, filenameAddition)
    let f = bufname("%")
    if f =~ '.'.a:fileExtension
        if f =~ '\<'.a:firstDirBeginning && f =~ a:filenameAddition.'\.'.a:fileExtension
            let filename = substitute(substitute(f, a:firstDirBeginning, '', ''), a:filenameAddition, '', '')
            if !filereadable(filename)
                let new_dir = substitute(filename, '/\w\+\.'.a:fileExtension, '', '')
                exe ":!mkdir -p ".new_dir
            endif
            exe ":e ".filename
        elseif f =~ '\<'.a:secondDirBeginning && f !~ a:filenameAddition.'\.'.a:fileExtension
            let filename = substitute(substitute(f, a:secondDirBeginning, a:firstDirBeginning.a:secondDirBeginning, ''), '.'.a:fileExtension, a:filenameAddition.'.'.a:fileExtension, '')
            if !filereadable(filename)
                let new_dir = substitute(filename, '/\w\+'.a:filenameAddition.'\.'.a:fileExtension, '', '')
                exe ":!mkdir -p ".new_dir
            endif
            exe ":e ".filename
        else
            echom "Could not switch because needed patterns not matched."
        endif
    endif
endfunction

I hope you’ll profit from it.