Utilizzo di NSpec in vari livelli architettonici

4

Dopo aver letto l'avvio rapido su nspec.org, mi sono reso conto che NSpec potrebbe essere uno strumento utile in uno scenario che stava diventando un po 'ingombrante con NUnit da solo.

Sto aggiungendo un OAuth (o, DotNetOpenAuth) a un sito web e rapidamente ho fatto un casino di scrivere metodi di test come

[Test]
public void UserIsLoggedInLocallyPriorToInvokingExternalLoginAndExternalLoginSucceedsAndExternalProviderIdIsNotAlreadyAssociatedWithUserAccount()
{
...
}

... e mi sono ritrovato con una dozzina di permutazioni di questo tema, dato che l'utente si stava già loggando localmente e non localmente, l'accesso esterno aveva esito positivo o negativo, ecc. Non solo i nomi dei metodi erano ingombranti, ma ogni il test richiedeva una configurazione che contenesse parti in comune con una serie diversa di altri test.

Mi sono reso conto che le funzionalità di configurazione incrementale di NSpec avrebbero funzionato benissimo per questo, e per un certo periodo stavo trasportando un lungo meraviglioso, con codice come

  act = () => { actionResult = controller.ExternalLoginCallback(returnUrl); };

            context["The user is already logged in"] = () =>
            {
                before = () => identity.Setup(x => x.IsAuthenticated).Returns(true);
                context["The external login succeeds"] = () =>
                {
                    before = () => oauth.Setup(x => x.VerifyAuthentication(It.IsAny<string>())).Returns(new AuthenticationResult(true, providerName, "provideruserid", "username", new Dictionary<string, string>()));
                    context["External login already exists for current user"] = () =>
                    {
                        before = () => authService.Setup(x => x.ExternalLoginExistsForUser(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(true);
                        it["Should add 'login sucessful' alert"] = () =>
                        {
                            var alerts = (IList<Alert>)controller.TempData[TempDataKeys.AlertCollection];
                            alerts[0].Message.should_be_same("Login successful");
                            alerts[0].AlertType.should_be(AlertType.Success);
                        };
                        it["Should return a redirect result"] = () => actionResult.should_cast_to<RedirectToRouteResult>();
                    };
                    context["External login already exists for another user"] = () =>
                    {
                        before = () => authService.Setup(x => x.ExternalLoginExistsForAnyOtherUser(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(true);
                        it["Adds an error alert"] = () =>
                        {
                            var alerts = (IList<Alert>)controller.TempData[TempDataKeys.AlertCollection];
                            alerts[0].Message.should_be_same("The external login you requested is already associated with a different user account");
                            alerts[0].AlertType.should_be(AlertType.Error);
                        };
                        it["Should return a redirect result"] = () => actionResult.should_cast_to<RedirectToRouteResult>();
                    };

Questo approccio ha funzionato magnificamente fino a quando non mi sono preparato a scrivere il codice di test per il mio livello ApplicationServices, al quale delegare la manipolazione viewmodel dai miei controller MVC e che coordina le operazioni del livello di repository dati inferiore:

public void CreateUserAccountFromExternalLogin(RegisterExternalLoginModel model)
{
    throw new NotImplementedException();
}

public void AssociateExternalLoginWithUser(string userName, string provider, string providerUserId)
{
    throw new NotImplementedException();
}

public string GetLocalUserName(string provider, string providerUserId)
{
    throw new NotImplementedException();
}

Non ho idea di quale sia il mondo per nominare la classe di test, i metodi di test, o anche se dovrei includere il test per questo livello nella classe di test dal mio frammento di codice grande sopra, in modo che una singola funzione o l'azione dell'utente potrebbe essere testata senza riguardo per la stratificazione architettonica.

Non riesco a trovare tutorial o post di blog che coprano più di semplici esempi, quindi gradirei eventuali consigli o indicassi la giusta direzione. Mi piacerebbe anche che la risposta "la tua domanda non sia valida" risponda a una domanda purché sia fornita una spiegazione.

    
posta ardave 09.12.2012 - 08:37
fonte

1 risposta

1

Attenzione: non ho mai usato NSpec, ma lo uso sempre per il cugino RSpec. È la mia esperienza con quello strumento che mi porta alla seguente risposta.

Suggerisco di utilizzare questa convenzione di denominazione:

class describe_your_class : nspec
{
  void instance_method_CreateUserAccountFromExternalLogin()
  {
    before = () => 
    {
      account_repository = new AccountRepository();
      fixture = new YourClass(account_repository);
    }
    context["with a valid external login model"] = () =>
    {
      before = () => 
      {
        account = CreateValidAccountForTesting();
        fixture.CreateUserAccountFromExternalLogin(account);
      }
      it["creates a user account"] = () => account_repository.count.should_be(1);
    }
    context["with a null external login model"] = () =>
    {
      before = () =>
      {
        fixture.CreateUserAccountFromExternalLogin(null);
      }
      it["silently does not create a user"] = () => account_repository.count.should_be(0);
    }
  }
}

In RSpec questo sarebbe:

describe YourClass do
  # "#method_name" is a convention that's used in Ruby docs to refer to an
  # instance method. ".method_name" would refer to a class method
  describe '#create_user_account_from_external_login' do
    before do
      # the "@" here indicates that these are instance variables
      @account_repository = AccountRepository.new
      @fixture = YourClass.new(@account_repository)
    end

    context 'with a valid external login' do
      before do
        account = create_valid_account_for_testing()
        @fixture.create_user_account_from_external_login(account)
      end
      it 'creates a user account' do
        @account_repository.count.should == 1
      end
    end

    context 'with a nil external login' do
      before do
        @fixture.create_user_account_from_external_login(nil)
      end
      it 'silently does not create a user account' do
        @account_repository.count.should == 0
      end
    end
  end
end

Ci sono altri costrutti forniti da RSpec, quindi probabilmente non scriverò le mie specifiche esattamente come sopra, ma sarei in grado di capire qualcun altro che ho trovato. C'è un grande sito focalizzato su RSpec chiamato Better Specs che potrebbe darti più ispirazione. Dovrai tradurre parte della sintassi, naturalmente.

    
risposta data 09.06.2013 - 04:35
fonte

Leggi altre domande sui tag