Pergunta

Com o código, formulários e dados dentro do mesmo banco de dados, estou me perguntando quais são as melhores práticas para projetar um conjunto de testes para um aplicativo Microsoft Access (digamos, para o Access 2007).

Um dos principais problemas com os formulários de teste é que apenas alguns controles têm um hwnd handle e outros controles recebem apenas um deles em foco, o que torna a automação bastante opaca, já que você não pode obter uma lista de controles em um formulário para agir.

Alguma experiência para compartilhar?

Foi útil?

Solução

1.Escreva código testável

Primeiro, pare de escrever lógica de negócios no code-behind do seu formulário.Esse não é o lugar para isso.Não pode ser testado adequadamente lá.Na verdade, você realmente não deveria ter que testar seu próprio formulário.Deve ser uma visão simples e burra que responda à interação do usuário e então delegue a responsabilidade de responder a essas ações para outra classe que é testável.

Como você faz isso?Familiarizando-se com o Padrão Model-View-Controller é um bom começo.

Model View Controller diagram

Isso não pode ser feito perfeitamente no VBA devido ao fato de obtermos eventos ou interfaces, nunca ambos, mas você pode chegar bem perto.Considere este formulário simples que possui uma caixa de texto e um botão.

simple form with text box and button

No code-behind do formulário, agruparemos o valor do TextBox em uma propriedade pública e geraremos novamente quaisquer eventos nos quais estivermos interessados.

Public Event OnSayHello()
Public Event AfterTextUpdate()

Public Property Let Text(value As String)
    Me.TextBox1.value = value
End Property

Public Property Get Text() As String
    Text = Me.TextBox1.value
End Property

Private Sub SayHello_Click()
    RaiseEvent OnSayHello
End Sub

Private Sub TextBox1_AfterUpdate()
    RaiseEvent AfterTextUpdate
End Sub

Agora precisamos de um modelo para trabalhar.Aqui eu criei um novo módulo de classe chamado MyModel.Aqui está o código que colocaremos em teste.Observe que ele naturalmente compartilha uma estrutura semelhante à nossa visão.

Private mText As String
Public Property Let Text(value As String)
    mText = value
End Property

Public Property Get Text() As String
    Text = mText
End Property

Public Function Reversed() As String
    Dim result As String
    Dim length As Long

    length = Len(mText)

    Dim i As Long
    For i = 0 To length - 1
        result = result + Mid(mText, (length - i), 1)
    Next i

    Reversed = result
End Function

Public Sub SayHello()
    MsgBox Reversed()
End Sub

Finalmente, nosso controlador conecta tudo.O controlador escuta eventos de formulário e comunica alterações ao modelo e aciona as rotinas do modelo.

Private WithEvents view As Form_Form1
Private model As MyModel

Public Sub Run()
    Set model = New MyModel
    Set view = New Form_Form1
    view.Visible = True
End Sub

Private Sub view_AfterTextUpdate()
    model.Text = view.Text
End Sub

Private Sub view_OnSayHello()
    model.SayHello
    view.Text = model.Reversed()
End Sub

Agora este código pode ser executado a partir de qualquer outro módulo.Para os fins deste exemplo, usei um módulo padrão.Eu recomendo fortemente que você crie isso sozinho usando o código que forneci e veja-o funcionar.

Private controller As FormController

Public Sub Run()
    Set controller = New FormController
    controller.Run
End Sub

Então, isso é ótimo e tudo mas o que isso tem a ver com testes?! Amigo, tem tudo a ver com testes.O que fizemos foi tornar nosso código testável.No exemplo que forneci, não há razão alguma para tentar testar a GUI.A única coisa que realmente precisamos testar é o model.É aí que está toda a lógica real.

Então, vamos para a etapa dois.

2.Escolha uma estrutura de teste de unidade

Não há muitas opções aqui.A maioria das estruturas exige a instalação de suplementos COM, muitos padrões, sintaxe estranha, escrita de testes como comentários, etc.Por isso me envolvi construindo um eu mesmo, portanto, esta parte da minha resposta não é imparcial, mas tentarei fornecer um resumo justo do que está disponível.

  1. Unidade de conta

    • Funciona apenas no Access.
    • Requer que você escreva testes como um estranho híbrido de comentários e código.(sem inteligência para a parte do comentário.
    • é uma interface gráfica para ajudá-lo a escrever esses testes de aparência estranha.
    • O projeto não viu nenhuma atualização desde 2013.
  2. Unidade VB LiteNão posso dizer que o usei pessoalmente.Está disponível, mas não recebe uma atualização desde 2005.

  3. xlUnitxlUnit não é horrível, mas também não é bom.É desajeitado e há muitos códigos padronizados.É o melhor dos piores, mas não funciona no Access.Então, isso está fora.

  4. Construa sua própria estrutura

    Eu tenho estive lá e fiz isso.Provavelmente é mais do que a maioria das pessoas deseja, mas é completamente possível construir uma estrutura de teste de unidade em código VBA nativo.

  5. Estrutura de teste de unidade do suplemento Rubberduck VBE
    Isenção de responsabilidade:Sou um dos co-desenvolvedores.

    Sou tendencioso, mas este é de longe o meu favorito do grupo.

    • Pouco ou nenhum código padrão.
    • Intellisense está disponível.
    • O projeto está ativo.
    • Mais documentação do que a maioria desses projetos.
    • Ele funciona na maioria dos principais aplicativos de escritório, não apenas no Access.
    • Infelizmente, é um complemento COM, portanto deve ser instalado em sua máquina.

3.Comece a escrever testes

Então, de volta ao nosso código da seção 1.O único código que nós realmente precisava testar era o MyModel.Reversed() função.Então, vamos dar uma olhada em como seria esse teste.(O exemplo dado usa Rubberduck, mas é um teste simples e pode ser traduzido para a estrutura de sua escolha.)

'@TestModule
Private Assert As New Rubberduck.AssertClass

'@TestMethod
Public Sub ReversedReversesCorrectly()

Arrange:
    Dim model As New MyModel
    Const original As String = "Hello"
    Const expected As String = "olleH"
    Dim actual As String

    model.Text = original

Act:
    actual = model.Reversed

Assert:
    Assert.AreEqual expected, actual

End Sub

Diretrizes para escrever bons testes

  1. Teste apenas uma coisa de cada vez.
  2. Bons testes só falham quando há um bug introduzido no sistema ou quando os requisitos mudam.
  3. Não inclua dependências externas, como bancos de dados e sistemas de arquivos.Essas dependências externas podem fazer com que os testes falhem por motivos fora do seu controle.Em segundo lugar, eles tornam seus testes mais lentos.Se seus testes forem lentos, você não os executará.
  4. Use nomes de teste que descrevam o que o teste está testando.Não se preocupe se demorar.É mais importante que seja descritivo.

Eu sei que a resposta foi um pouco longa e atrasada, mas espero que ajude algumas pessoas a começar a escrever testes de unidade para seu código VBA.

Outras dicas

Apreciei as respostas de Knox e David.Minha resposta estará em algum lugar entre a deles:apenas faça formulários que não precisam ser depurados!

Eu acho que os formulários devem ser usados ​​exclusivamente como são basicamente, ou seja, interface gráfica apenas, o que significa aqui que eles não precisam ser depurados!O trabalho de depuração é então limitado aos módulos e objetos VBA, o que é muito mais fácil de manusear.

É claro que existe uma tendência natural de adicionar código VBA a formulários e/ou controles, especialmente quando o Access oferece esses ótimos eventos "após atualização" e "na mudança", mas eu definitivamente aconselho você não para colocar qualquer formulário ou código específico de controle no módulo do formulário.Isso torna a manutenção e a atualização adicionais muito caras, onde seu código é dividido entre módulos VBA e módulos de formulários/controles.

Isso não significa que você não possa mais usar isso AfterUpdate evento!Basta colocar o código padrão no evento, assim:

Private Sub myControl_AfterUpdate()  
    CTLAfterUpdate myControl
    On Error Resume Next
    Eval ("CTLAfterUpdate_MyForm()")
    On Error GoTo 0  
End sub

Onde:

  • CTLAfterUpdate é um procedimento padrão executado sempre que um controle é atualizado em um formulário

  • CTLAfterUpdateMyForm é um procedimento específico executado cada vez que um controle é atualizado no MyForm

Eu tenho então 2 módulos.O primeiro é

  • utilityFormEvents
    onde terei meu evento genérico CTLAfterUpdate

O segundo é

  • MyAppFormEvents
    contendo o código específico de todos os formulários específicos do aplicativo MyApp e incluindo o procedimento CTLAFTERUPDATEMYFORM.Obviamente, o ctlafterupdatemyform pode não existir se não houver código específico para ser executado.É por isso que transformamos o "erro" para "retomar a próxima" ...

Escolher uma solução tão genérica significa muito.Isso significa que você está alcançando um alto nível de normalização de código (ou seja, manutenção de código sem problemas).E quando você diz que não possui nenhum código específico de formulário, isso também significa que os módulos de formulário são totalmente padronizados e sua produção pode ser automatizado:basta dizer quais eventos você deseja gerenciar no nível de formulário/controle e definir sua terminologia de procedimentos genéricos/específicos.
Escreva seu código de automação de uma vez por todas.
Demora alguns dias de trabalho, mas dá resultados emocionantes.Tenho usado esta solução nos últimos 2 anos e é claramente a certa:meus formulários são criados completa e automaticamente do zero com uma "Tabela de Formulários", vinculada a uma "Tabela de Controles".
Posso então dedicar meu tempo trabalhando nos procedimentos específicos do formulário, se houver.

A normalização do código, mesmo com o MS Access, é um processo longo.Mas realmente vale a pena a dor!

Outra vantagem de Acesso sendo um aplicativo COM é que você pode criar um Aplicativo .NET para executar e testar um aplicativo Access via automação.A vantagem disso é que você pode usar uma estrutura de teste mais poderosa, como NUunidade para escrever testes de afirmação automatizados em um aplicativo do Access.

Portanto, se você for proficiente em C# ou VB.NET combinado com algo como NUnit, poderá criar mais facilmente uma maior cobertura de testes para seu aplicativo Access.

Embora essa seja uma resposta muito antiga:

Unidade de conta, uma estrutura especializada de teste de unidade para Microsoft Access.

Eu tirei uma página de Teste do Python conceito e implementou um procedimento DocTests no Access VBA.Obviamente, esta não é uma solução completa de teste de unidade.Ainda é relativamente jovem, então duvido que tenha resolvido todos os bugs, mas acho que está maduro o suficiente para ser solto na natureza.

Basta copiar o código a seguir em um módulo de código padrão e pressionar F5 dentro do Sub para vê-lo em ação:

'>>> 1 + 1
'2
'>>> 3 - 1
'0
Sub DocTests()
Dim Comp As Object, i As Long, CM As Object
Dim Expr As String, ExpectedResult As Variant, TestsPassed As Long, TestsFailed As Long
Dim Evaluation As Variant
    For Each Comp In Application.VBE.ActiveVBProject.VBComponents
        Set CM = Comp.CodeModule
        For i = 1 To CM.CountOfLines
            If Left(Trim(CM.Lines(i, 1)), 4) = "'>>>" Then
                Expr = Trim(Mid(CM.Lines(i, 1), 5))
                On Error Resume Next
                Evaluation = Eval(Expr)
                If Err.Number = 2425 And Comp.Type <> 1 Then
                    'The expression you entered has a function name that ''  can't find.
                    'This is not surprising because we are not in a standard code module (Comp.Type <> 1).
                    'So we will just ignore it.
                    GoTo NextLine
                ElseIf Err.Number <> 0 Then
                    Debug.Print Err.Number, Err.Description, Expr
                    GoTo NextLine
                End If
                On Error GoTo 0
                ExpectedResult = Trim(Mid(CM.Lines(i + 1, 1), InStr(CM.Lines(i + 1, 1), "'") + 1))
                Select Case ExpectedResult
                Case "True": ExpectedResult = True
                Case "False": ExpectedResult = False
                Case "Null": ExpectedResult = Null
                End Select
                Select Case TypeName(Evaluation)
                Case "Long", "Integer", "Short", "Byte", "Single", "Double", "Decimal", "Currency"
                    ExpectedResult = Eval(ExpectedResult)
                Case "Date"
                    If IsDate(ExpectedResult) Then ExpectedResult = CDate(ExpectedResult)
                End Select
                If (Evaluation = ExpectedResult) Then
                    TestsPassed = TestsPassed + 1
                ElseIf (IsNull(Evaluation) And IsNull(ExpectedResult)) Then
                    TestsPassed = TestsPassed + 1
                Else
                    Debug.Print Comp.Name; ": "; Expr; " evaluates to: "; Evaluation; " Expected: "; ExpectedResult
                    TestsFailed = TestsFailed + 1
                End If
            End If
NextLine:
        Next i
    Next Comp
    Debug.Print "Tests passed: "; TestsPassed; " of "; TestsPassed + TestsFailed
End Sub

Copiar, colar e executar o código acima de um módulo chamado Module1 produz:

Module: 3 - 1 evaluates to:  2  Expected:  0 
Tests passed:  1  of  2

Algumas notas rápidas:

  • Não possui dependências (quando usado no Access)
  • Faz uso de Eval que é uma função no modelo de objeto Access.Application;isso significa que você poderia usá-lo fora do Access, mas exigiria a criação de um objeto Access.Application e a qualificação completa do Eval chamadas
  • Há alguns idiossincrasias associadas a Eval estar ciente de
  • Só pode ser usado em funções que retornem um resultado que caiba em uma única linha

Apesar de suas limitações, ainda acho que ele oferece um bom retorno para seu investimento.

Editar:Aqui está uma função simples com "regras doctest" que a função deve satisfazer.

Public Function AddTwoValues(ByVal p1 As Variant, _
        ByVal p2 As Variant) As Variant
'>>> AddTwoValues(1,1)
'2
'>>> AddTwoValues(1,1) = 1
'False
'>>> AddTwoValues(1,Null)
'Null
'>>> IsError(AddTwoValues(1,"foo"))
'True

On Error GoTo ErrorHandler

    AddTwoValues = p1 + p2

ExitHere:
    On Error GoTo 0
    Exit Function

ErrorHandler:
    AddTwoValues = CVErr(Err.Number)
    GoTo ExitHere
End Function

Eu projetaria o aplicativo para ter o máximo de trabalho possível em consultas e sub-rotinas vba, para que seus testes pudessem ser compostos de preenchimento de bancos de dados de teste, execução de conjuntos de consultas de produção e vba nesses bancos de dados e, em seguida, análise da saída e comparando para ter certeza de que a saída é boa.Obviamente, essa abordagem não testa a GUI, portanto, você pode aumentar o teste com uma série de scripts de teste (aqui quero dizer como um documento do Word que diz abrir o formulário 1 e clicar no controle 1) que são executados manualmente.

Depende do escopo do projeto quanto do nível de automação necessário para o aspecto de teste.

Se você estiver interessado em testar seu aplicativo Access em um nível mais granular, especificamente o próprio código VBA, então Unidade VB Lite é uma ótima estrutura de teste de unidade para esse propósito.

Acho que há relativamente poucas oportunidades para testes unitários em meus aplicativos.A maior parte do código que escrevo interage com os dados da tabela ou com o sistema de arquivamento, portanto é fundamentalmente difícil de testar a unidade.No início, tentei uma abordagem que pode ser semelhante à zombaria (spoofing), onde criei um código que tinha um parâmetro opcional.Se o parâmetro fosse usado, o procedimento usaria o parâmetro em vez de buscar dados no banco de dados.É muito fácil configurar um tipo definido pelo usuário que tenha os mesmos tipos de campo de uma linha de dados e passá-lo para uma função.Agora tenho uma maneira de inserir dados de teste no procedimento que desejo testar.Dentro de cada procedimento havia algum código que trocava a fonte de dados real pela fonte de dados de teste.Isso me permitiu usar testes unitários em uma ampla variedade de funções, usando minhas próprias funções de testes unitários.Escrever testes unitários é fácil, é apenas repetitivo e chato.No final, desisti dos testes unitários e comecei a usar uma abordagem diferente.

Eu escrevo aplicativos internos para mim principalmente para poder esperar até que os problemas me encontrem, em vez de ter que ter um código perfeito.Se eu escrever aplicativos para clientes, geralmente o cliente não está totalmente ciente de quanto custa o desenvolvimento de software, então preciso de uma forma de baixo custo para obter resultados.Escrever testes de unidade envolve escrever um teste que envia dados incorretos em um procedimento para ver se o procedimento pode tratá-los adequadamente.Os testes unitários também confirmam que bons dados são tratados de forma adequada.Minha abordagem atual é baseada em escrever validação de entrada em cada procedimento dentro de um aplicativo e gerar um sinalizador de sucesso quando o código for concluído com êxito.Cada procedimento de chamada verifica o sinalizador de sucesso antes de usar o resultado.Se ocorrer um problema, ele será relatado por meio de uma mensagem de erro.Cada função possui um sinalizador de sucesso, um valor de retorno, uma mensagem de erro, um comentário e uma origem.Um tipo definido pelo usuário (fr para retorno de função) contém os membros de dados.Qualquer função pode preencher apenas alguns dos membros de dados no tipo definido pelo usuário.Quando uma função é executada, ela geralmente retorna sucesso = verdadeiro e um valor de retorno e às vezes um comentário.Se uma função falhar, ela retornará sucesso = falso e uma mensagem de erro.Se uma cadeia de funções falhar, as mensagens de erro serão alteradas em série, mas o resultado será, na verdade, muito mais legível do que um rastreamento de pilha normal.As origens também estão encadeadas, então sei onde ocorreu o problema.O aplicativo raramente trava e relata quaisquer problemas com precisão.O resultado é muito melhor do que o tratamento de erros padrão.

Public Function GetOutputFolder(OutputFolder As eOutputFolder) As  FunctRet

        '///Returns a full path when provided with a target folder alias. e.g. 'temp' folder

            Dim fr As FunctRet

            Select Case OutputFolder
            Case 1
                fr.Rtn = "C:\Temp\"
                fr.Success = True
            Case 2
                fr.Rtn = TrailingSlash(Application.CurrentProject.path)
                fr.Success = True
            Case 3
                fr.EM = "Can't set custom paths – not yet implemented"
            Case Else
                fr.EM = "Unrecognised output destination requested"
            End Select

    exitproc:
        GetOutputFolder = fr

    End Function

Código explicado.eOutputFolder é um Enum definido pelo usuário conforme abaixo

Public Enum eOutputFolder
    eDefaultDirectory = 1
    eAppPath = 2
    eCustomPath = 3
End Enum

Estou usando Enum para passar parâmetros para funções, pois isso cria um conjunto limitado de opções conhecidas que uma função pode aceitar.Enums também fornecem inteligência ao inserir parâmetros em funções.Suponho que eles forneçam uma interface rudimentar para uma função.

'Type FunctRet is used as a generic means of reporting function returns
Public Type  FunctRet
    Success As Long     'Boolean flag for success, boolean not used to avoid nulls
    Rtn As Variant      'Return Value
    EM As String        'Error message
    Cmt As String       'Comments
    Origin As String    'Originating procedure/function
End Type

Um tipo definido pelo usuário, como FunctRet, também fornece preenchimento de código que ajuda.Dentro do procedimento, normalmente armazeno resultados internos em uma variável interna anônima (fr) antes de atribuir os resultados à variável de retorno (GetOutputFolder).Isso torna os procedimentos de renomeação muito fáceis, pois apenas a parte superior e inferior foram alteradas.

Então resumindo, desenvolvi um framework com ms-access que abrange todas as operações que envolvem VBA.O teste é permanentemente escrito nos procedimentos, em vez de um teste unitário de tempo de desenvolvimento.Na prática, o código ainda roda muito rápido.Tenho muito cuidado em otimizar funções de nível inferior que podem ser chamadas dez mil vezes por minuto.Além disso, posso usar o código em produção à medida que ele é desenvolvido.Se ocorrer um erro, é fácil de usar e a origem e o motivo do erro geralmente são óbvios.Os erros são relatados a partir do formulário de chamada, e não de algum módulo da camada de negócios, que é um princípio importante do design do aplicativo.Além disso, não tenho o fardo de manter o código de testes unitários, o que é muito importante quando estou evoluindo um design, em vez de codificar um design claramente conceituado.

Existem alguns problemas potenciais.O teste não é automatizado e novos códigos incorretos só são detectados quando o aplicativo é executado.O código não se parece com o código VBA padrão (geralmente é mais curto).Ainda assim, a abordagem tem algumas vantagens.É muito melhor usar um manipulador de erros apenas para registrar um erro, pois os usuários geralmente entrarão em contato comigo e me darão uma mensagem de erro significativa.Também pode lidar com procedimentos que funcionam com dados externos.JavaScript me lembra VBA, me pergunto por que JavaScript é a terra dos frameworks e VBA no ms-access não.

Alguns dias depois de escrever este post, encontrei um artigo sobre o CodeProject isso se aproxima do que escrevi acima.O artigo compara e contrasta o tratamento de exceções e o tratamento de erros.O que sugeri acima é semelhante ao tratamento de exceções.

Eu não tentei isso, mas você pode tentar publique seus formulários de acesso como páginas da web de acesso a dados para algo como o sharepoint ou assim como páginas da web e então use uma ferramenta como selênio para conduzir o navegador com um conjunto de testes.

Obviamente, isso não é tão ideal quanto conduzir o código diretamente por meio de testes de unidade, mas pode ajudar você em parte do caminho.boa sorte

O acesso é um aplicativo COM.Use COM, não API do Windows.para testar coisas no Access.

O melhor ambiente de teste para um aplicativo Access é o Access.Todos os seus formulários/relatórios/tabelas/código/consultas estão disponíveis, existe uma linguagem de script semelhante ao MS Test (ok, você provavelmente não se lembra do MS Test), existe um ambiente de banco de dados para armazenar seus scripts de teste e resultados de teste, e as habilidades que você desenvolve aqui são transferíveis para sua aplicação.

Há boas sugestões aqui, mas estou surpreso que ninguém tenha mencionado o processamento centralizado de erros.Você pode obter suplementos que permitem modelos rápidos de funções/submodelos e adicionar números de linha (eu uso ferramentas MZ).Em seguida, envie todos os erros para uma única função onde você poderá registrá-los.Você também pode interromper todos os erros definindo um único ponto de interrupção.

As páginas de acesso a dados foram descontinuadas pela MS há algum tempo e nunca funcionaram realmente (elas dependiam da instalação dos widgets do Office e funcionavam apenas no IE, e apenas mal).

É verdade que os controles do Access que podem obter o foco só possuem um identificador de janela quando têm o foco (e aqueles que não conseguem obter o foco, como rótulos, nunca possuem um identificador de janela).Isso torna o Access singularmente inadequado para regimes de testes controlados por janelas.

Na verdade, questiono por que você deseja fazer esse tipo de teste no Access.Parece-me o seu dogma básico de Extreme Programming, e nem todos os princípios e práticas do XP podem ser adaptados para funcionar com aplicativos Access - pino quadrado, buraco redondo.

Portanto, dê um passo atrás e pergunte-se o que você está tentando realizar e considere que pode ser necessário utilizar métodos completamente diferentes daqueles baseados em abordagens que simplesmente não funcionam no Access.

Ou se esse tipo de teste automatizado é válido ou mesmo útil com um aplicativo Access.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top