【问题标题】:Is it possible to use Cypress e2e testing with a firebase auth project?是否可以将 Cypress e2e 测试与 firebase auth 项目一起使用?
【发布时间】:2018-07-21 23:30:15
【问题描述】:

我正在探索赛普拉斯进行 e2e 测试,看起来很棒的软件。 问题是身份验证,赛普拉斯文档解释了为什么使用 UI 非常糟糕here

所以我尝试查看我的应用程序的网络分流器,看看我是否可以向 firebase API 创建一个 POST 请求,并在不使用 GUI 的情况下进行身份验证。但我可以看到至少有 2 个请求被触发,并且令牌保存到应用程序存储中。

那么我应该使用什么方法呢?

  1. 使用我的应用程序的 UI 进行身份验证,并指示赛普拉斯不要接触本地存储
  2. 继续尝试发送正确 POST 请求的方法,并将值保存到本地存储。
  3. 让 Cypress 运行自定义 JS 代码,然后使用 Firebase SDK 登录。

我真的在这里寻求一些建议:)

【问题讨论】:

  • 只是评论指出,Firebase 似乎已更改其实现以将身份验证数据存储在 indexedDB 而不是 localStorage 中,因此已接受的解决方案将不再有效。
  • 嗨@Dygerati 感谢您指出这一点,我会调查
  • @Dygerati 你找到解决方案了吗?我仍然无法在 cypress 测试中访问我的数据
  • @NateMay 我必须对访问数据做同样的事情,我发布的答案包括执行此操作的自定义命令。

标签: firebase firebase-authentication cypress


【解决方案1】:

当我自己执行此操作时,我创建了自定义命令(例如 cy.login 用于身份验证,然后 cy.callRtdbcy.callFirestore 用于验证数据)。在厌倦了重复构建它们所需的逻辑之后,我将它包装到一个名为 cypress-firebase 的库中。它包括自定义命令和生成自定义身份验证令牌的 cli。

设置主要包括在cypress/support/commands.js 中添加自定义命令:

import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/database';
import 'firebase/firestore';
import { attachCustomCommands } from 'cypress-firebase';

const fbConfig = {
    // Your config from Firebase Console
};

window.fbInstance = firebase.initializeApp(fbConfig);

attachCustomCommands({ Cypress, cy, firebase })

并将插件添加到cypress/plugins/index.js:

const cypressFirebasePlugin = require('cypress-firebase').plugin

module.exports = (on, config) => {
  // `on` is used to hook into various events Cypress emits
  // `config` is the resolved Cypress config

  // Return extended config (with settings from .firebaserc)
  return cypressFirebasePlugin(config)
}

但是有关于设置are available in the setup docs的完整详细信息。

披露,我是cypress-firebase的作者,这就是全部答案。

【讨论】:

  • 太棒了 - 所以在设置完之后你可以简单地打电话给cy.login?
  • @DauleDK 没错!自从我的团队使用它以来,我也在积极开发更多功能。这是一篇解释如何设置它的文章:medium.com/@prescottprue/…。披露:我是答案中讨论的工具和此评论中发布的文章的作者。
  • 是的,我们使用它在 CI 上运行所有测试。我们发现 Gitlab 是运行赛普拉斯测试的最简单设置。
【解决方案2】:

我采取了使用自动化 UI 的方法来获取 Firebase JS SDK 使用的 localStorage 的内容。我还想在整个 Cypress 运行时只执行一次,所以我在 Cypress 开始之前就这样做了。

  1. 通过 pupeteer 获取 Firebase SDK localStorage 条目
  2. 将内容存储在 tmp 文件中(通过 env var 将其传递给 Cypress 时出现问题)
  3. 通过 env var 将文件位置传递给 Cypress,让它读取内容并设置 localStorage 以设置会话

获取localStorage内容的辅助脚本:

const puppeteer = require('puppeteer')

const invokeLogin = async page => {
    await page.goto('http://localhost:3000/login')

    await page.waitForSelector('.btn-googleplus')
    await page.evaluate(() =>
        document.querySelector('.btn-googleplus').click())
}

const doLogin = async (page, {username, password}) => {

    // Username step
    await page.waitForSelector('#identifierId')
    await page.evaluate((username) => {
        document.querySelector('#identifierId').value = username
        document.querySelector('#identifierNext').click()
    }, username)

    //  Password step
    await page.waitForSelector('#passwordNext')
    await page.evaluate(password =>
            setTimeout(() => {
                document.querySelector('input[type=password]').value = password
                document.querySelector('#passwordNext').click()
            }, 3000) // Wait 3 second to next phase to init (couldn't find better way)
        , password)
}

const extractStorageEntry = async page =>
    page.evaluate(() => {
        for (let key in localStorage) {
            if (key.startsWith('firebase'))
                return {key, value: localStorage[key]}
        }
    })

const waitForApp = async page => {
    await page.waitForSelector('#app')
}

const main = async (credentials, cfg) => {
    const browser = await puppeteer.launch(cfg)
    const page = await browser.newPage()

    await invokeLogin(page)
    await doLogin(page, credentials)
    await waitForApp(page)
    const entry = await extractStorageEntry(page)
    console.log(JSON.stringify(entry))
    await browser.close()
}

const username = process.argv[2]
const password = process.argv[3]

main({username, password}, {
    headless: true // Set to false for debugging
})

由于将 JSON 作为环境变量发送到赛普拉斯存在问题,我使用 tmp 文件在脚本和赛普拉斯进程之间传递数据。

node test/getFbAuthEntry ${USER} ${PASSWORD} > test/tmp/fbAuth.json
cypress open --env FB_AUTH_FILE=test/tmp/fbAuth.json

在 Cypress 中,我从文件系统中读取它并将其设置为 localStorage

const setFbAuth = () =>
    cy.readFile(Cypress.env('FB_AUTH_FILE'))
        .then(fbAuth => {
            const {key, value} = fbAuth
            localStorage[key] = value
        })

describe('an app something', () => {
    it('does stuff', () => {
        setFbAuth()
        cy.viewport(1300, 800)
...

【讨论】:

  • 这个解决方案非常有趣,因为我可以看到您使用 google 作为身份验证提供程序。我只使用电子邮件和密码,因此使用 API 实际上相对容易。性能如何?
  • 是的。在使用它的项目中,有 3 个身份验证提供者——Facebook、谷歌和电子邮件。我不想依赖 Firebase 的内部结构,因为它会使测试变得更加脆弱。设置需要 cca 5 秒,但没关系,因为所有测试只完成一次。
  • 不幸的是,Firebase 刚刚切换到 indexedDB,因此这个本地存储解决方案不再有效。
【解决方案3】:

这当然是一个 hack,但为了绕过我正在开发的应用程序的登录部分,我使用 beforeEach 挂钩登录到应用程序。

beforeEach(() => {
  cy.resetTestDatabase().then(() => {
    cy.setupTestDatabase();
  });
});

这来自我的辅助函数。

Cypress.Commands.add('login', () => {
  return firebase
    .auth()
    .signInWithEmailAndPassword(Cypress.env('USER_EMAIL'), Cypress.env('USER_PASSWORD'));
});

Cypress.Commands.add('resetTestDatabase', () => {
  return cy.login().then(() => {
    firebase
      .database()
      .ref(DEFAULT_CATEGORIES_PATH)
      .once('value')
      .then(snapshot => {
        const defaultCategories = snapshot.val();
        const updates = {};
        updates[TEST_CATEGORIES_PATH] = defaultCategories;
        updates[TEST_EVENTS_PATH] = null;
        updates[TEST_STATE_PATH] = null;
        updates[TEST_EXPORT_PATH] = null;

        return firebase
          .database()
          .ref()
          .update(updates);
      });
  });
});

我想知道的是,从firebase 返回的信息最终如何保存到localStorage。我对此并没有真正的答案,但它确实有效。此外,该应用程序使用.signInWithPopup(new firebase.auth.GoogleAuthProvider()),而在上面它使用电子邮件和密码登录。所以我只是因为cypress 有 CORS 限制,所以我有点简化了登录过程。

【讨论】:

    【解决方案4】:

    即将推出的Auth emulator 让这变得更容易。Firebase Auth Emulator (firebase-tools >= 8.1.4) 让这变得更容易。

    cypress/support/signAs.js:

    Cypress.Commands.add('signAs', (uid, opt) => {
      cy.visit('/')
    
      cy.window().its('firebase').then( fb => {
        cy.wrap( (async _ => {
          // Create a user based on the provided token (only '.uid' is used by Firebase)
          await fb.auth().signInWithCustomToken( JSON.stringify({ uid }) );
    
          // Set '.displayName', '.photoURL'; for email and password, other functions exist (not implemented)
          await fb.auth().currentUser.updateProfile(opt);
        })() )
      })
    })
    

    将其用作:

    cy.signAs('joe', { displayName: 'Joe D.', photoURL: 'http://some' });
    

    如果您需要设置.email.password,它们有类似的功能,但这对于我的测试来说已经足够了。作为测试的一部分,我现在可以临时模拟任何用户。该方法不需要在模拟器中创建用户;您可以使用特定的 uid 声称自己是其中之一。很适合我。

    注意:

    Firebase 身份验证在 IndexedDB 中(如其他答案中所述),赛普拉斯在测试之间没有清除它。 cypress #1208 中对此进行了讨论。

    【讨论】:

    • 更新:auth 模拟器现在在 v.8.14 中可用
    • 嘿,我的做法基本相同,但使用模拟器时它会抱怨同源策略并且无法调用 localhost:9099 - 你知道这种情况下的问题是什么吗?
    • @Ianwen 不知道。您是否使用 demo-... 项目名称来确保 Firebase 不会尝试访问云?此外,客户端在 9.x 中发生了很大变化。 Here 是上述代码 sn-p 的演变过程。我记得要做到这一点很棘手,但不记得同源问题。
    【解决方案5】:

    在撰写本文时,我已经研究了这些方法

    • 存根 firebase 网络请求 - 真的很难。连续发送一堆firebase请求。请求参数过多,负载过大,无法读取。
    • localStorage 注入 - 与请求存根相同。它需要对 Firebase SDK 和数据结构有深入的了解。
    • cypress-firebase 插件 - 它不够成熟且缺乏文档。我跳过了这个选项,因为它需要一个服务帐户(管理员密钥)。我正在做的项目是开源的,有很多贡献者。如果不将密钥包含在源代码管理中,就很难共享密钥。

    最终,我自己实现了它,这非常简单。最重要的是,它不需要任何机密的 Firebase 凭据。基本上,它是由

    完成的
    • 在 Cypress 中初始化另一个 firebase 实例
    • 使用该 firebase 实例构建赛普拉斯自定义命令以登录

    const fbConfig = {
      apiKey: `your api key`, // AIzaSyDAxS_7M780mI3_tlwnAvpbaqRsQPlmp64
      authDomain: `your auth domain`, // onearmy-test-ci.firebaseapp.com
      projectId: `your project id`, // onearmy-test-ci
    
    }
    firebase.initializeApp(fbConfig)
    
    const attachCustomCommands = (
      Cypress,
      { auth, firestore }: typeof firebase,
    ) => {
      let currentUser: null | firebase.User = null
      auth().onAuthStateChanged(user => {
        currentUser = user
      })
    
      Cypress.Commands.add('login', (email, password) => {
        Cypress.log({
          displayName: 'login',
          consoleProps: () => {
            return { email, password }
          },
        })
        return auth().signInWithEmailAndPassword(email, password)
      })
    
      Cypress.Commands.add('logout', () => {
        const userInfo = currentUser ? currentUser.email : 'Not login yet - Skipped'
        Cypress.log({
          displayName: 'logout',
          consoleProps: () => {
            return { currentUser: userInfo }
          },
        })
        return auth().signOut()
      })
    
    }
    
    attachCustomCommands(Cypress, firebase)
    

    这是包含所有集成代码https://github.com/ONEARMY/community-platform/commit/b441699c856c6aeedb8b73464c05fce542e9ead1的提交

    【讨论】:

    • 这看起来已经过时了。它对我不起作用,而且看起来该代码库也已经更新了解决方案。它似乎不适用于同一窗口上的两个 firebase auth 实例,所以我和那个代码库也通过窗口 (github.com/ONEARMY/community-platform/commit/…) 将应用程序中的 firebase 实例共享到 Cypress
    【解决方案6】:

    好的,经过多次尝试和错误,我尝试了解决方案路径 2,它成功了。

    所以我的身份验证流程如下所示:

    1. 发送 POST 请求(使用 cybress.request)到 https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword, 并解析响应。创建对象:response1 = response.body

    2. 发送 POST 请求(使用 cybress.request)到 https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo, 使用上一个请求中的 idToken。创建对象:user = response2.body.users[0];

    将响应与以下属性组合在一个对象中:

    const authObject = {
      uid: response1.localId,
      displayName: response1.displayName,
      photoURL: null,
         email: response1.email,
         phoneNumber: null,
         isAnonymous: false,
         providerData: [
           {
              uid: response1.email,
              displayName: response1.displayName,
              photoURL: null,
              email: body.email,
              phoneNumber: null,
              providerId: 'password'
           }
          ],
          'apiKey': apiKey,
          'appName': '[DEFAULT]',
          'authDomain': '<name of firebase domain>',
          'stsTokenManager': {
             'apiKey': apiKey,
             'refreshToken': response1.refreshToken,
             'accessToken': response1.idToken,
             'expirationTime': user.lastLoginAt + Number(response1.expiresIn)
           },
           'redirectEventId': null,
           'lastLoginAt': user.lastLoginAt,
           'createdAt': user.createdAt
        };
    

    然后在 cybress 中,我只是将这个对象保存在本地存储中,在 before 钩子中:localStorage.setItem(firebase:authUser:${apiKey}:[DEFAULT], authObject);

    也许并不完美,但它解决了问题。如果您对代码感兴趣,请告诉我,如果您对如何构建“authObject”有任何了解,或者以其他方式解决此问题。

    【讨论】:

    • 不幸的是,Firebase 刚刚切换到 indexedDB,因此这个本地存储解决方案不再有效。
    猜你喜欢
    • 2019-08-22
    • 2021-02-04
    • 1970-01-01
    • 2022-11-11
    • 2022-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-07
    相关资源
    最近更新 更多